背景
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
には期待し過ぎない方が良さそうです。 act
は root
ユーザとしてコマンド実行しますが、 GitHub Actions では runner
ユーザとしてコマンド実行するなど、大きく動作が異なります。結局、本家 GitHub Actions でデバッグすることになりました。
まさか 100 回も CI を空回りさせる ことになるとは……。
テストを実行する
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 されている (後述) GHCup を使用するため、何もしない
- GHC, Cabal: キャッシュしない
- Cabal store (依存パッケージのキャッシュ): キャッシュする
dist-newstyle/
(ユーザのライブラリのキャッシュ): キャッシュしない
例に加えて、 dist-newstyle/
もキャッシュする形でセットアップしました。
oj-verify
を並列実行する
【競プロ】ライブラリの verify を GitHub Actions で並列に走らせたい (oj-verify) #C++ - Qiita を参考に、 oj-verify
を並列実行しました。 oj-verify
はオンラインジャッジの問題をローカル実行するためのツールで、今日の ac-library-hs
では 85 個のテストを実行します。
GitHub Actions の実行環境は CPU が弱い (2 コア) ため、複数の環境で並列実行した方が早く終わります。そのため matrix 機能を使います:
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 の setup action だとこうは行きません (後述) 。
- 4: verify 用ソースファイルから担当ファイルを抜き出します。
- 5: 抜き出したファイルを
oj-verify
にかけます。j
オプションにより、 job 内でも 2 コア CPU で並列実行できているはずです。
また cabal test
と doctest
の実行を別の job に分けました。どの job でも Haskell のセットアップや build
ジョブで生成したキャッシュを取得するため、 composite action を共有しました。
キャッシュの見直し
haskell-actions@setup@v2
が毎回 GHC のインストールに 2 分かけています。キャッシュも上手く行きません。なんとか workaround を見つけました。
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 のキャッシュを考えていないようで、キャッシュに一手間かかります。留意点としては:
- 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 が変わります。haskell-actions/setup@v2
は必ず GHC を再インストールするため、 GHC のインストールをスキップするには action 全体を飛ばす必要があります。
この辺りの理解が大変でした。自分で GHCup を呼び出した方が簡単だと思います。いっそ Nix を使っても良い気がします。
まとめ
簡単な GitHub Actions をセットアップしました。 Runner images の環境の理解や、 haskell-actions/setup@v2
の非自明な挙動に悩まされました。真の runner-image
を、ローカルで、 interactive に実行できたら良かったと思います。
ハンズオン後は GitHub CI/CD実践ガイド が頭に入りやすくなりました。全然脳みその滑りが違います (?) 。 GitHub Actions は未だに未成熟な印象でしたが、キャッシュのサイズを始め、どんどんと良くなっているようです。 Job の関係が DAG になっているのが面白く、色々なシステムが似たような仕組みで構築されている気がします。