まえがき
2019年のCIはBuddyを使うっぽいので、がんばる。
ちゅうい
この記事は最低限のことしか書いていない。過度に期待してはならない。
Haskell Stack じゃなくて Cabal じゃん
Buddy 公式のクイックスタートドキュメント CI/CD for Haskellがstackではなくcabalを使ったやり方だったので、寝たら忘れてしまうだろうから、この記事にメモっておく。
プロジェクトを作成する
Haskell Stackでプロジェクトの作成
stack new プロジェクト名
cd プロジェクト名
Githubにpushする
Githubにリポジトリを作り、pushする
# このへんでgit remoteの設定してから
git add .
git commit -m 'first commit'
git push
TDD しているフリをする
ひとまずテストが動くか試す
stack test
コマンドを実行して、 Test suite not yet implemented
が印字されたら「テストスイートがまだ未実装」が成功だ。
stack test
依存ライブラリの設定
hspec
を使うのでpackage.yaml
にhspec
を追加する。
tests:
プロジェクト名-test:
main: Spec.hs
source-dirs: test
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- プロジェクト名
- hspec
*Spec.hs を根こそぎimportされるようにする
手動ですべてのSpecファイルをimportしないで済むように、 テストプロジェクトのメインファイル test/Spec.hs
を修正する。
GHCの魔法
test/Spec.hs
を次の1行のみにすると、GHCの魔法がかかって*Spec.hs
がなぜかimportされるようになる。
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
詳しくは、Automatic spec discoveryを参照。
LibSpec.hsの作成
src
ディレクトリーにLib.hs
があるので、それに対応するLibSpec.hs
をtest
ディレクトリーに作成する。
作成する関数は、みんな大好きフィボナッチ関数だ。
module LibSpec (spec) where
import Test.Hspec
import Lib
spec :: Spec
spec = do
describe "fib :: Int -> Int" $ do
context "fib 0" $ do
it "should be 0" $ do
fib 0 `shouldBe` 0
テストがコンパイルエラーになるのを確認する
stack test
を実行して、Haskell初心者が気を失うようなコンパイルエラーが印字されたら成功だ。
stack test
# ...略
/Users/jdoiwork/github/try-buddy-ci/test/LibSpec.hs:11:9: error:
• Variable not in scope: fib :: Integer -> Integer
• Perhaps you meant ‘fit’ (imported from Test.Hspec)
|
11 | fib 0 `shouldBe` 0
| ^^^
なぜコンパイルエラーになるのだろう? 当然だ。 テストコードは書いたが、肝心のfib
関数を実装していないからだ。
fib関数を仮実装する
src/Lib.hs
を修正して、fib
関数を仮実装しよう。 ついでにstack new
で自動生成されたsomeFunc
関数は消してしまおう。
module Lib
( fib
) where
fib :: Int -> Int
fib _ = 777
app
側もsomeFunc
を使っているコードがあるので、適当に消してしまおう。
module Main where
import Lib
main :: IO ()
main = print "Hello Main"
テストがエラー(RED)になるのを確認する
stack test
を実行して、テストがエラーになったら成功だ。
stack test
# ...略
Lib
fib :: Int -> Int
fib 0
should be 0 FAILED [1]
Failures:
test/LibSpec.hs:11:9:
1) Lib, fib :: Int -> Int, fib 0, should be 0
expected: 0
but got: 777
To rerun use: --match "/Lib/fib :: Int -> Int/fib 0/should be 0/"
Randomized with seed 302256979
Finished in 0.0033 seconds
1 example, 1 failure
わざとらしく赤くしたところでグリーンにしよう。だってTDDだもん。
fib 0を実装する
ケント・ベックの本を読んだ人なら、これくらい一歩ずつ進むハズだ。
fib :: Int -> Int
fib 0 = 0
fib _ = 777
テストが成功(GREEN)になるのを確認する
stack test
を実行して、テストが成功になったら成功だ。
stack test
# ...略
Lib
fib :: Int -> Int
fib 0
should be 0
Finished in 0.0002 seconds
1 example, 0 failures
TDDに夢中になりそうなので、git commit
とgit push
したら、そろそろBuddyを働かせよう。
Buddy CI
https://buddy.works/ に行く。
「ユーザー登録」と「プロジェクトの作成」は、大雑把にしか説明しないから、詳しく知りたい人は [CI] Buddy を始めましたを見よう。
ユーザー登録
ページ中央付近の「Sign up for free with...」 で github を指定してボタンをポチポチするだけでOKだ。
プロジェクトの作成
今作ったgithubリポジトリを指定する。
パイプラインの作成
Add a new pipeline
ボタンを押して、NAME
を適当に入力して、TRIGGER MODE
をOn push
にする。

入力内容を確認したら、フォームの下の方のAdd a new pipeline
ボタンを押す。
アクションの作成
テストを実行するためのアクションを作成する。

Haskellアクションの選択
フィルターテキスト入力にhaskell
と打って、アクションを絞り込んでHaskell
をクリックする。

Run 設定の編集
BUILD COMMANDS
のテキストエリアがcabal
を使う形になっているので、以下のようにstack test
に変更する。

Environment 設定の編集
Environment
ボタンをクリックして、Image
をfpco/stack-build-small
、Version
をlatest
(または任意のバージョン)にする。
他のDocker Imageがいい人は、 https://hub.docker.com/u/fpco 辺りを探すか自作する。

アクションの追加
下の方へスクロールして、Add this action
ボタンをクリックしてアクションを追加する。

パイプラインの完成
おめでとう、最初のパイプラインの完成だ。

パイプラインの実行
右上のRun pipeline
ボタンを押すと、パイプライン実行フォームが出るのでRun now
ボタンをクリックして、パイプラインを実行する。

パイプライン実行結果
Logs
ボタンをクリックして、hspec
の成功とBuild finished successfully!.
とグリーンになっていたら成功だ。

実装を再開する
最低限の環境が整ったので、実装を再開する。 これからは手動でパイプラインを実行しないでも、 Github にpush
されたらパイプラインが実行される設定になっているので、git push
するたびにオートでやってくれるはずだ。
パイプラインが失敗するのを確認する
失敗するテスト(未実装のfib 1
のテストコード)を書いて、パイプラインが正常に失敗するのを確認しよう。
spec :: Spec
spec = do
describe "fib :: Int -> Int" $ do
context "fib 0" $ do
it "should be 0" $ do
fib 0 `shouldBe` 0
-- ↓を追加
context "fib 1" $ do
it "should be 1" $ do
fib 1 `shouldBe` 1
ローカルで失敗するのを確認する
stack test
を実行して、テストが失敗するのを確認したら、git commit
とgit push
をして、パイプラインを動かそう。
stack test
# ...略
Lib
fib :: Int -> Int
fib 0
should be 0
fib 1
should be 1 FAILED [1]
Failures:
test/LibSpec.hs:14:9:
1) Lib, fib :: Int -> Int, fib 1, should be 1
expected: 1
but got: 777
To rerun use: --match "/Lib/fib :: Int -> Int/fib 1/should be 1/"
Randomized with seed 435405822
Finished in 0.0015 seconds
2 examples, 1 failure
パイプラインの失敗

よし! 正しく失敗できたぞ!!
パイプラインが成功に戻るのを確認する
fib 1を実装して、テストをグリーンにする
fib
関数の続きを実装しよう。
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib _ = 777
stack test
を実行して、ローカルでテストが成功するのを確認する。
stack test
# ...略
Lib
fib :: Int -> Int
fib 0
should be 0
fib 1
should be 1
Finished in 0.0004 seconds
2 examples, 0 failures
pushしてパイプラインが成功するのを確認する
git commit
とgit push
をしたら、Buddyで新たにパイプラインが実行されるので成功するのを確認しよう。

やった! グリーンだよ!! これで最低限のCI環境が整った(個人の感想です)。
継続的にインテグる
さらなる fib へ
あとは気の済むまでフィボナッチ関数を実装しよう。 うっかりテストを通さずにpush
しても間違いに気づきやすくなるかもしれない。
spec :: Spec
spec = do
describe "fib :: Int -> Int" $ do
context "fib 0" $ do
it "should be 0" $ do
fib 0 `shouldBe` 0
context "fib 1" $ do
it "should be 1" $ do
fib 1 `shouldBe` 1
context "fib 2" $ do
it "should be 1" $ do
fib 2 `shouldBe` 1
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2) -- TODO: SICPを最後まで読んで、もっと効率のいい実装にする

依存ライブラリのビルド時間を短縮する
この章は、オマケだ。 気が長いならやらなくてもよい。
現状の単純なパイプラインだと、パイプラインが実行されるたびに依存ライブラリがビルドされてしまう。テストの実行時間より依存ライブラリのビルド時間の方が長いのは、致命的な致命傷だ。
この問題を解決するために、次のようにパイプラインを2つに分ける。
- 依存ライブラリのビルド
- コンパイル済み依存ライブラリのコピー → テスト実行
依存ライブラリのビルドパイプラインの作成
パイプライン一覧に行き、一覧の右のサイドメニューから Clone existing pipeline
を選択して既存のパイプラインを複製する。

作成済みのパイプラインを選択して Clone actions
ボタンをクリックしてパイプラインを複製する。
依存ライブラリのみビルドする
次の手順の通りにして、依存ライブラリのみビルドされるようにする。
- パイプライン一覧で複製したパイプラインを選択する
-
Actions
設定を選択する -
stack test
していたHaskellアクションを選択する -
BUILD COMMANDS
をstack test --only-dependencies --stack-root $(pwd)/.stack-root
にする

Save this action
ボタンをクリックして、アクション保存する。
パイプライン設定の変更
ついでに、パイプライン名をわかりやすいものに変更し、パイプラインが走るタイミングも手動に変更する。
-
Settings
設定をクリックする -
NAME
を適当なものに編集する -
TRIGGER MODE
をManual
にする - 下の方へスクロールして
Save settings
ボタンを押して保存する。

今回は手動でパイプラインが走れば十分だが、定期的にパイプラインを走らせたければTRIGGER MODE
をRecurrently
にすると定期実行設定が選べる。
依存ライブラリのパス
依存ライブラリの保存場所は次の2ヶ所。
./.stack-work
$HOME/.stack
Buddy上では(というかdockerコンテナ内だから?)、$HOME/.stack
が/root/.stack
になってしまい、後からアクセスできない場所に出力されてしまうため(セキュリティ上の理由か?)、--stack-root
オプションで出力場所を作業ディレクトリーの.stack-root
に変更している。
stack test --only-dependencies --stack-root $(pwd)/.stack-root
依存ライブラリビルド用パイプラインの実行
パイプライン一覧から作成した依存ライブラリビルド用パイプラインを選択し、 右上の Run pipeline
ボタンをクリックしてパイプラインを実行しよう。

ビルドが成功し、テストが実行されていなければ(hspec
のログが出ていなければ)OKだ。
ビルド済み依存ライブラリのコピー
テストを走らせるパイプラインに戻り、.stack-work
と.stack-root
をコピーするアクションを2つ作成する。
既存のアクションの上下にギアのアイコンの+
ボタンがあるので、それをクリックするとアクションの追加ができる。

stack-root Copyアクションの作成
フィルターテキスト入力にcopy
と打って、Copy files
アクションを選択する。

設定値を次の表のようにする。
設定 | 値 | 備考 |
---|---|---|
Pipeline | さっき作った依存ライブラリビルド用パイプライン | コピー元のパイプラインを指定する |
Provide the path to the files that you want to copy | .stack-root/ | コピー元のファイル Browse... で探すのが楽ちん |
Provide the destination path | .stack-root | コピー先のパス |

入力したら、フォームの下の方のAdd this action
ボタンをクリックして、アクションを作成する。
stack-work Copyアクションの作成
同様にstack-work用のCopyアクションを作成する。
設定値を次の表のようにして、アクションを作成する。
設定 | 値 | 備考 |
---|---|---|
Pipeline | さっき作った依存ライブラリビルド用パイプライン | コピー元のパイプラインを指定する |
Provide the path to the files that you want to copy | .stack-work/ | コピー元のファイル Browse... で探すのが楽ちん |
Provide the destination path | (何も入力しないでOK) | コピー先のパス |

ビルド済み依存ライブラリを使うようにする
stack test
アクションを選択して、BUILD COMMANDS
を次のようにする。
--stack-root
と--allow-different-user
オプションを追加している。
stack test --stack-root $(pwd)/.stack-root --allow-different-user

--allow-different-user
オプションを指定しないと、セキュリティ意識が高いのか依存ライブラリがビルドし直されてしまうので注意。
Save this action
ボタンをクリックして保存する。
速くなった(たぶん)パイプラインを実行する
右上の Run pipeline
ボタンをクリックして、パイプラインを実行する。上手くできていることを願う。

パイプラインの実行時間の比較
ほとんどライブラリを使っていないプロジェクトでも2分弱かかっていたのが、数秒〜十数秒に短縮された。
やったぜ!

分割したパイプライン
見事、依存ライブラリのビルド用パイプラインとテスト実行用のパイプラインに分けることができた。

今後の課題
- deployがんばれ