背景
GitHub Actions 入門編です。 ac-library-hs
の CI を設定してみました。個人開発のため CI を使う意味はほぼ無いですが、競プロ・ Haskell 関連のプラクティスを収集できて良かったです。
今回作成した workflow は、 GitHub 上では次のように表示します:

build
後に test
, doctest
, verify
job を並列実行します
この設定の作り方を大まかに記載します。
初心者のノートです。
CI の設定
環境構築
GitHub Actions の設定にあたり、以下のツールを使用しました:
- nektos/act
GitHub Actions のローカル実行用ツールです。 - adrienverge/yamllint
YAML の構文エラー検出ツールです。 - rhysd/actionlint
Workflow ファイルの (弱い) 静的解析ツールです。 gh
(cli/cli)
gh cache
でキャッシュ関連の操作ができます。
act
では root
ユーザとしてコマンド実行しますが、 GitHub Actions では runner
ユーザとしてコマンド実行するなど、かなり動作が異なります。結局、本家 GitHub Actions でデバッグしましたが、まさか 100 回も空回りさせる ことになるとは……。
テストを実行する
haskell-actions
の examples
を参考に CI に入門しました。 Minimal な例として以下が挙げられています:
on: [push]
name: build
jobs:
runhaskell:
name: Hello World
runs-on: ubuntu-latest # or macOS-latest, or windows-latest
steps:
- uses: actions/checkout@v4 # 1
- uses: haskell-actions/setup@v2 # 2
- run: runhaskell Hello.hs # 3
- 1: リポジトリのファイルをダウンロードします。
- 2: GHC, Cabal 等をインストールする action を起動します (バージョン指定もできます) 。
- 3: シェルコマンドを実行できます。テスト実行なども書けます。
ここまではあっさりと設定できました。
ところで、
haskell-actions/setup@v2
には LLVM 版 Haskell をインストールする方法が無さそうです。ビルドが長くなるので必要無いかもしれませんが、出鼻をくじかれました。
依存パッケージをキャッシュする
Model cabal workflow with caching の例は、整理すると次の方針で書かれています:
- GHCup: preinstall されている (後述) ため、何もしない
- GHC, Cabal: キャッシュしない
- Cabal store (依存パッケージのキャッシュ): キャッシュする
dist-newstyle/
: キャッシュしない
加えて dist-newstyle/
もキャッシュする形でセットアップしました。
oj-verify
を並列実行する
【競プロ】ライブラリの verify を GitHub Actions で並列に走らせたい (oj-verify) #C++ - Qiita を参考に、 oj-verify
を並列実行しました。 oj-verify
はオンラインジャッジの問題をローカル実行するためのツールで、僕の環境では 85 個のテストを実行します。 GitHub Actions の実行環境は CPU が弱い (2 コア) ため、複数の環境で並列実行した方が早く終わります:
build:
# ~~
verify:
needs: build
env:
ghc-version: '9.8.4'
num-parallel: '15'
# ~~
strategy:
fail-fast: false
matrix:
parallel-index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] # 1
steps:
- uses: actions/checkout@v4
# ~~
# Haskell のインストール、キャッシュの読み込みなど # 2
# ~~
- name: Setup Python
uses: actions/setup-python@v5 # 3
with:
python-version: '3.13'
- name: Install oj-verify
run: pip3 install -U online-judge-verify-helper
- name: Run oj-verify
working-directory: verify
run: |
files="$(find app/ -type f | awk 'NR % ${{ env.num-parallel }} == ${{ matrix.parallel-index }}')" # 4
oj-verify run $files --tle 30 -j $(nproc) # 5
- 1: 複数 (15) の job に分けて実行します。
- 2:
verify
はbuild
job とは隔離された環境で実行されるため、あらためて Haskell (GHC) をセットアップします。 - 3: Preinstall 済みの Python を PATH に入れるだけなので、一瞬で終わります。便利!
haskell-actions/setup@v2
は必ず新しい GHC/Cabal をダウンロード・ビルドするので、改善の余地があります (#119) 。どちらかと言えば、 GitHub Actions という仕組みそのものに改善の余地がありそうです。 - 4: verify 用ソースファイルから担当ファイルを抜き出します。
- 5: 抜き出したファイルを
oj-verify
にかけます。j
オプションにより、 job 内でも 2 コア CPU で並列実行できているはず。
cabal test
と doctest
の実行も別の job に分けました。どの job でも Haskell のセットアップや build
ジョブで生成したキャッシュを取得するため、 composite action を共有しました。
キャッシュの見直し
Runner image
GitHub Actions の実行環境 (runner-images
参照) の preinstalled tool を確認します。今日の install-haskell.sh
では、次のように GHCup をインストールしています:
# ~~
export GHCUP_INSTALL_BASE_PREFIX=/usr/local
# ~~
url --proto '=https' --tlsv1.2 -fsSL https://get-ghcup.haskell.org | sh > /dev/null 2>&1 || true
haskell/ghcup-hs
を見れば、 該当行 で GHCUP_INSTALL_BASE_PREFIX
が使われています。よって /usr/local/.ghcup/bin/ghcup
がインストールされている他、一部バージョンの GHC や Cabal も preinstall されています。
また由来は不明ですが、 GitHub Actions の実行環境には ~/.ghcup -> /usr/local/.ghcup
の symlink があります。そのため /usr/local/.ghcup/bin/ghcup install *
すると /usr/local/.ghcup/bin
にツールがインストールされ、 ~/.ghcup/bin
としても見えるようになります。んな〜〜
Preinstall 版の cabal
を使う
haskell-actions/setup@v2
で毎回 GHC のダウンロードに 2 分かかるので、 GHC をキャッシュしてみました。 haskell-actions/setup@v2
は GHC のキャッシュを考えていないようで、一手間かかります。留意点としては:
- Cabal や GHC のバージョンが変わるとキャッシュが無効になるため、確実にバージョンを揃える必要があります。
haskell-actions/setup@v2
には Cabal をインストールしないオプションがありません。そのため実行後は~/.ghcup/bin/cabal
が上書きされ、 preinstall 済みのcabal
を指さなくなります。haskell-actions/setup@v2
は起動時に Cabal の XDG path mode を強制的に無効化します 。したがってhaskell-actions/setup@v2
を使う場合、使わない場合で cabal store の path が変わります。
この辺の理解が大変でした。自分で GHCup を呼び出した方が簡単だったかもしれません。いっそ Nix を使っても良い気がします。 Nix による CI は僕の Nixify your devlog や GitHub ActionsでCachixを導入する などに載っています。
まとめ
簡単な GitHub Actions をセットアップしました。 Runner images の環境の理解や、 haskell-actions/setup@v2
の非自明な挙動に悩まされました。真の runner-image
をローカルで、 interactive に実行できたら良かったと思います。 Nix でセットアップできないものでしょうか。
ハンズオン後は GitHub CI/CD実践ガイド が頭に入りやすくなりました。全然脳みその滑りが違いますね (?) 。 GitHub Actions は未だに未成熟な印象でしたが、キャッシュのサイズを始め、どんどん良くなっているようです。 Job の関係が DAG になっているのが面白く、色々なシステムが似たような仕組みで構築されている気がします。