背景

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

今回作成した workflow は、 GitHub 上では次のように表示します:

2025-05-12-ci.png
Figure 1: build 後に test, doctest, verify job を並列実行します

この設定の作り方を大まかに記載します。

初心者のノートです。

CI の設定

環境構築

GitHub Actions の設定にあたり、以下のツールを使用しました:

act では root ユーザとしてコマンド実行しますが、 GitHub Actions では runner ユーザとしてコマンド実行するなど、かなり動作が異なります。結局、本家 GitHub Actions でデバッグしましたが、まさか 100 回も空回りさせる ことになるとは……。

テストを実行する

haskell-actionsexamples を参考に 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

ここまではあっさりと設定できました。

ところで、 haskell-actions/setup@v2 には LLVM 版 Haskell をインストールする方法が無さそうです。ビルドが長くなるので必要無いかもしれませんが、出鼻をくじかれました。

依存パッケージをキャッシュする

Model cabal workflow with caching の例は、整理すると次の方針で書かれています:

加えて 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

cabal testdoctest の実行も別の 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 のキャッシュを考えていないようで、一手間かかります。留意点としては:

この辺の理解が大変でした。自分で GHCup を呼び出した方が簡単だったかもしれません。いっそ Nix を使っても良い気がします。 Nix による CI は僕の Nixify your devlogGitHub ActionsでCachixを導入する などに載っています。

まとめ

簡単な GitHub Actions をセットアップしました。 Runner images の環境の理解や、 haskell-actions/setup@v2 の非自明な挙動に悩まされました。真の runner-image をローカルで、 interactive に実行できたら良かったと思います。 Nix でセットアップできないものでしょうか。

ハンズオン後は GitHub CI/CD実践ガイド が頭に入りやすくなりました。全然脳みその滑りが違いますね (?) 。 GitHub Actions は未だに未成熟な印象でしたが、キャッシュのサイズを始め、どんどん良くなっているようです。 Job の関係が DAG になっているのが面白く、色々なシステムが似たような仕組みで構築されている気がします。