まえがき
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がんばれ