GitHub Actions で Pull Request のチェックをする
前回の記事 で GitHub Actions を使って push の度に test を動かす方法を紹介しました。
今回は、 Pull Request で実行する Workflow を作ってみたいと思います。
(2018/12/24 現在、 Pull Request での実行は private repository のみでの提供となっています。)
Pull Request をワークフローのトリガーにする
Pull Request で Workflow を実行するには、on
attribute を pull_request
にします。
workflow "NAME_OF_WORKFLOW" { on = "pull_request" ... }
その他、対応しているトリガーを確認したい場合 公式ドキュメント に詳しく記述されています。
※ Pull Request の場合、 Review Request( review_requested
) や タイトルの変更( edited
) など、Pull Request に関わる多くのイベントに対して実行されます。Pull Request 内のコミットに対してのみワークフローを実行したい場合、Actionでは synchronized
に対して実行するなどのフィルターが必要になるでしょう。
Event Types & Payloads | GitHub Developer Guide
上記のようにAction内で更に特定の条件を追加したい場合、Payload を扱うことができます。Payload は JSON形式のファイルとして 環境変数 GITHUB_EVENT_PATH
のパスに配置されています。(
Accessing the runtime environment | GitHub Developer Guide )
コンテナ内で jq
コマンドなどを利用することで、Eventに関する特定の情報を取得することができます。
例えば以下のコマンドで Pull Request 番号 を取得することができます。
cat $GITHUB_EVENT_PATH | jq -r ".number"
GITHUB_TOKEN を払い出す
今回 Pull Request のチェックに利用しようとしている Danger は、Pull Request に対してコメントをすることができます。
その他 Pull Request の情報を取得するために Personal Access Token が必要になります。
Personal Access Token を利用してコメントを行った場合、トークンを払い出したアカウントがコメントを行うことになります。
チームなどで開発する際は bot account を作成し、そちらからトークンを払い出す。といった運用を行うことになります。
これが、 GitHub Actions では GITHUB_TOKEN
という環境変数で払い出されます。(Storing secrets | GitHub Developer Guide)
こちらを利用することで、 bot account を作成しなくても人に依存しないトークンを利用することができます。 個人的に非常に魅力的な機能で、 GitHub Actions ならではの優位性だと感じています。
利用するには action ブロックで secrets
に GITHUB_TOKEN
を追加します。
action "Danger" { ... secrets = ["GITHUB_TOKEN"] }
Action を作成する
Danger を利用して Pull Request をチェックする Action を作成してみましょう。
前回の記事でも紹介したとおり、Dockerfileで定義します。
FROM ruby:2.5-alpine LABEL "repository"="https://github.com/duck8823/actions" LABEL "homepage"="https://github.com/duck8823/actions" LABEL "maintainer"="<shunsuke maeda> duck8823@gmail.com" LABEL "com.github.actions.name"="Danger" LABEL "com.github.actions.description"="Run Danger" LABEL "com.github.actions.icon"="alert-triangle" LABEL "com.github.actions.color"="yellow" RUN apk update \ && apk add git \ && rm -rf /var/cache/apk/* RUN gem install danger ADD . . CMD "danger"
必要な依存が最低限と、実行コマンドのシンプルな Dockerfile です。
LABEL を利用することで、 GUI での見た目を変更することができます。
ここでは Danger というツールを使うので、それっぽいアイコンにてみました。
icon は 以下のものが利用できるようです。
feathericons.com
なお、 Danger では 各CI に対してクラスを定義する仕組みなので、上記 Pull Request 番号の取得方法などを踏まえて PR を出しました。既にマージしていただいているので、利用することができます。
Action を利用する
定義した Action の利用方法は前回の記事のとおりですが、今回は汎用的な処理なので別のリポジトリに定義したActionを配置しました。
github.com
Action は action ブロックの uses
で定義しますが、指定方法もいくつかあります。
(Workflow configuration options | GitHub Developer Guide)
GitHub に置かれた Action であれば {owner}/{repo}/{Dockerfileが置かれたディレクトリまでのpath}@{ref}
で指定できます。
action "Danger" { uses = "duck8823/actions/danger@master" ... }
Danger で Pull Request をチェックする
Danger は Dangerfile というファイルを定義し、その内容によって Pull Request の内容をチェック・コメントするツールです。
warn("diff が大きいぞ") if git.lines_of_code > 500
以下の記事で簡単に紹介しています。
duck8823.hatenablog.com
記事内容では bundler でインストールしていますが、作成した Action ではコンテナに直接 danger をインストールしています。
プラグインなどを利用したい場合は、 bundler/inline
を利用して実行時にインストールすることができるでしょう。
実際に Danger を実行し、定義したチェックを全てパスすると Commit Status が作成されます。その際 GitHub Actions で払い出されたTokenを利用したので @github-actions
が作成したことになっています。
出来上がったワークフロー
完成したワークフローは以下のとおりです。かなりシンプルに表現できていると思います。 対象リポジトリで行うことは、下記ワークフローの定義と、実際にチェックをするための Dangerfile の作成になります。
workflow "Check Pull Request" { resolves = ["Danger"] on = "pull_request" } action "Danger" { uses = "duck8823/actions/danger@master" secrets = ["GITHUB_TOKEN"] }
まとめ
GitHub Actions の Token で Danger 実行するのに成功。
— だっく ?: 🦆 (@duck8823) 2018年12月21日
これ、bot用アカウント作らなくても色々できるようになるので最の高やで。 pic.twitter.com/ZU0jqLesbo
おまけ
さて、最初に紹介したとおり pull_request
をトリガーにするとレビューワーの追加などでもワークフローが実行されます。 CommitStatus はそれぞれ別に作成するようで...
GitHub Actions が pull request で発火するようにしてて、レビュワー追加したら発火されまくって一人で爆笑してる。 pic.twitter.com/MS4kqoToBs
— だっく ?: 🦆 (@duck8823) 2018年12月21日
GitHub Actions で push の度にテストを動かす
GitHub Actions が public repository でも使えるようになりました!
(2018/12/24現在まだ public beta 版での提供です。)
早速、現在アクティブに開発しているリポジトリで試してみました。
対象のリポジトリには既に以下の様な Dockerfile
があります。
FROM golang:1.11-alpine MAINTAINER shunsuke maeda <duck8823@gmail.com> RUN apk --update add --no-cache alpine-sdk WORKDIR /go/src/github.com/duck8823/duci ADD . . ENV CC=gcc ENV CI=duci ENV GO111MODULE=on ENTRYPOINT ["make"] CMD ["test"]
必要な依存ライブラリなどをインストールし、 Makefile
で定義されたタスクを実行するだけです。
ちなみに Golang で開発しているリポジトリなので、 Makefile
では以下のようにテストの実行を定義しています。
test: go test -coverprofile cover.out $$(go list ./... | grep -v mock_) go tool cover -html cover.out -o cover.html
タスクランナーでタスクを定義し、 Dockerfile で実行環境を定義するという方針で開発をしています。
さて、この Dockerfile
を GitHub Actions で実行すれば、 Push の度にテストを実行してくれるということになりますね。
設定したいリポジトリの Actions
タブをクリックすると、GUIで編集できる画面になります。
Create a new workflow
をクリックして編集してみましょう。
直感的に操作できるようになっています。
最初からあるのはトリガーです。今はパブリックリポジトリでは pushしか選べません(2018/12/20 現在)。 本能に従いトリガーの青い部分をドラッグ&ドロップすると、 Action を定義する箇所が現れます。
ここで、Dockerfile
が存在するディレクトリの相対パスを指定することで、リポジトリ内の Dockerfile
を利用することができます。
.duci/Dockerfile
を実行したいとすると、 ./.duci/
となります。
use
をクリックすると更に詳細を設定できます。
GitHubの各リポジトリではSecrets が登録できるようになっており、ここで指定することでコンテナに渡すことができます。
また、Dockerflle
の ENTRYPOINT
や CMD
を上書きもできるようです。
ここではシンプルに何も上書き・設定しません。
GUI で編集したものは、定義ファイルとして保存されることになります。
workflow "New workflow" { on = "push" resolves = ["./duci/"] } action "./duci/" { uses = "./duci/" }
なにやら見慣れない形式ですね。
リポジトリの .github/main.workflow
というファイルが作成され、GUI上からコミットやブランチ&PRの作成までできてしまいます。
これをコミットして実際に push すると、 Commit Status もつけてくれます。
また、 Commit Status の details や Actions の log から、実行時のログを見ることもできます。
本来、色々な Action を組み合わせて workflow を組み立てていくものですが、ここでは push の度にテストを回す方法を紹介しました。
テスト実行用の Dockerfile を用意してGitHub上でちょっと操作するだけで CIでテストが回せる時代になってしまいましたね。
書いたテストはガンガン実行していきましょう。
Spring Boot WebアプリケーションをCI上でスモークテストする
機能も作り込んだ。テストもオールグリーン。さあ張り切ってみんなの前でデモするぞ...
$ ./gradlew bootRun
...
エラー
ってこと、ありませんか?
僕はありました。
できるだけ実行コストをかけないように、ユニットテストではSpringに依存しないようにしています。
もちろん結合テストなどでは @SpringBootTest
を利用していますがアプリケーション起動時のエラーは確認できません。
そんな中、以前 Flyway を Webアプリケーションを起動しないで 実行する方法を調べていたことを思いました。
※上記の記事時点では Spring Boot 1.5 なので、若干変わっています。
現在(2018/11/23時点)だと以下になります。
$ ./gradlew bootRun --args='--spring.main.web-application-type=none'
はい、あとはもうおわかりですね。
上記のコマンドをお使いの CI上で実行するだけです。
Webサーバーの立ち上げをしないで Spring Boot アプリケーションを起動することができます。
このとき、起動に失敗すれば 0 以外の Exit Code を返してくれるので、 CI では失敗として通知してくれます。
Flyway の DDL を修正や追加したときも、失敗したらCIコケるので便利でした。
一見目的が違うけど、「あー、あれ使ったらこれ達成できるなー。」って瞬間が好き。
Golang で CI を作っている話
ペライチ300行だったコードはコミットを重ね、少しずつ大きくなってきています。
リポジトリの名前も minimal-ci から duci に変えました。命名はお世話になっている先輩です。
コンセプト
CIは非常に便利です。僕も業務や趣味で CircleCI や Travis CI 、 Jenkins を利用しています。
ですが、これらの設定は非常に複雑でかつドメイン特異的になります。
CIのバージョンアップや移行で疲弊したこともあるのではないでしょうか。
そこでこのCIのコンセプトは DSL is Unnecessary For CI にしました。
- タスクはタスクランナーで定義しましょう
- タスクの実行に必要な環境は Dockefile で定義しましょう
- duci はコンテナ内でタスクを実行するだけです
僕の中で常に意識していることとして、開発中のフィードバックをできるだけ早くするというのがあります。
CIで失敗したときにCIでしか再実行・確認ができないのは辛いです。
同じ定義でローカルでも実行できるようにしています。
タスクはタスクランナーで定義しましょう
CIに特異的な設定をしないでジョブの定義をしましょう。
ローカルマシンでの開発中、多くの言語ではタスクランナーを利用すると思います。
Java、Kotlin では maven や gradle、JavaScript では npm や yarn がそれに当たります。
いずれのツールもサブコマンドで、予め定めたタスクを実行します。
Golangの場合はそれ自体のサブコマンドとして task
や build
が用意されています。
Makefile
でもタスクの定義が可能ですね。
https://github.com/duck8823/duci/blob/master/Makefile
make test
このタスクを CI サーバーでも実行するようにすれば、
ローカルでもCIでも同じタスクを実行することができます。
タスクの実行に必要な環境は Dockerfile で定義しましょう
ローカル開発時とCIで同じタスクが実行できるようになりました。
しかし、このままではローカルとサーバーで実行環境が異なります。
コンテナを利用しましょう。
Dockerfile を利用してタスクの実行環境を定義することで、開発に必要な依存も管理することができます。
FROM golang:1.11-alpine RUN apk --update add --no-cache alpine-sdk WORKDIR /go/src/github.com/duck8823/duci ADD . . ENV CC=gcc ENV GO111MODULE=on ...
Dockerfileをみるだけで必要な言語のバージョン、サーバーの依存(alpine-sdk)や環境変数がわかります。
これをコミットすることで必要な依存を変更したときも、どのコミットからその依存が必要になったか追えるようになります。
duci はコンテナ内でタスクを実行するだけです
コンテナ内ではタスクを実行するだけです。では、どのようにしてタスクを振り分けるのか。
Dockerfile
内の ENTRYPOINT
にタスクランナーのコマンド、 CMD
にデフォルトのサブコマンドを定義します。
そうすることで docker run
時にコマンドを渡さなければデフォルトのサブコマンド、コマンドを渡すことで特定のサブコマンドを実行することができます。
例えば Dockerfile
を以下のように定義します。
... ENTRYPOINT ["go"] CMD ["test", "./..."]
このとき、
docker run <IMAGE_NAME>
で コンテナ内では
go test ./...
が実行されます。
docker run <IMAGE_NAME> build
で
go build
が実行されます。
duci では GitHub の Webhook からジョブを実行しますが、
push
の場合はデフォルトのサブコマンド、issue_comment
の場合はコメントで指定したサブコマンドを実行するようにしました。
Dockerfile があれば、ローカルでもコンテナを起動して実行することができます。
CIで失敗したときにローカルですぐに試せるので、原因の解明がとても楽です。
CIの設定を試すために何度もコミット&プッシュする必要はありません。
パッケージ構成
コード量も増えたので、パッケージ構成も変更しました。
いわゆる階層アーキテクチャやクリーンアーキテクチャのような構成ではありませんが、自分なりに各パッケージに意味をもたせています。
- インフラストラクチャパッケージ
- 更に下位のパッケージに分けています。
それぞれのパッケージはアプリケーションに依存しないものを集めており、使い回ししやすくしています。
- 更に下位のパッケージに分けています。
- アプリケーションパッケージ
- アプリケーションの設定などの他、ランナー=メインロジック( Gitクローン -> Docker Run -> Commit Status ) を実現しています。
- データモデルパッケージ
- アプリケーションパッケージとプレゼンテーションパッケージで利用する構造体を定義しています。
- プレゼンテーションパッケージ
- ルーティングとハンドラーを定義しています。
ハンドラーは Webhooks をアプリケーションパッケージに渡す形に変換する役割を担っています。
- ルーティングとハンドラーを定義しています。
自分で考えたパッケージ構成にしておくことで、判断基準が自分にあるので悩まないですみます。
現段階のパッケージ構成なので、ソースが大きくなったりしたらまた変わるでしょう。
CIをもうちょっと高機能にする
必要最低限な機能は 前回の記事 で紹介しています。
実用的にするには、前回のペライチスクリプトではいくつかの機能が足りていません。
- 非同期的に実行する
- 並列数を制御する
- ジョブのタイムアウトを設定する
Webhooks -> Git Clone -> Archive Tar Package -> Docker Image Build -> Docker container create & run -> Create Commit Status
この一連の流れを同期実行していました。レスポンスはすべての処理が終わったあとに返します。
CIを実行するのにはそれなりに時間がかかるので、GitHubのWebhookでTimeoutと表示されてしまっていました。
そのため、各ジョブは非同期で実行できるようにしました。
非同期で実行する場合、サーバーのリソースは限られているのでジョブの並列数を意識する必要があります。
また、ジョブの最大並列数を定義した場合、各ジョブのタイムアウトを設定しなければキューに入れられたジョブがずっと実行されないままになる危険があります。
これらは比較的簡単に実現できました。そう、 goroutine ならね。
var sem = make(chan struct{}, runtime.NumCPU()) // 最大並列数=サーバーのコア数
go func ... { // goroutine で非同期に実行できるようにする ... errs := make(chan error, 1) timeout, cancel := context.WithTimeout(context.Background(), 30 * time.Second) // タイムアウトの設定 defer cancel() go func() { sem <- struct{}{} // 実行できる並列数を一つ減らす errs <- runner.Run(timeout) // メインの処理... 終了時に `errs` に渡す <-sem // 実行できる並列数を一つ戻す }() select { // タイムアウト or エラーが入るまで(メイン処理が終わるまで) case <-timeout.Done(): // タイムアウトの処理 return timeout.Err() case err := <-errs: if err != nil { // ジョブが失敗したときの処理 return err } // ジョブが成功したときの処理 return nil } }()
非同期にするのは関数呼び出し時に go
をつけるだけです。
タイムアウトは context.WithTimeout
で実現できます。メインの処理は更に goroutine とし、 select
でタイムアウトあるいはメインの処理実行まで待ちます。
並列数は最大実行可能数を渡した channel を作成しておき、メイン処理の前後で増やす・減らす処理をするだけで実現が可能です。
テストの話
テストについては golang.tokyo #17 で発表させて頂きました。
www.slideshare.net
まとめ
ペライチで作ったCIサーバーを育てていて、context とか goroutine の扱いを意識することができました。
MobyというDockerクライアントが含まれるライブラリも存在し、非同期処理や並列数も結構簡単に設定できるので、GolangはCIづくりに適してそうですね。
みなさんも自分好みのCIを作りましょう。
ペライチ300行弱のコードで簡易CIサーバーを作った
皆さんはゴールデンウィークいかがお過ごしでしょうか。
僕は暇すぎたので雑にCIサーバーのようなものを作ることにしました。
CIサーバーを運用する際の悩みとして、サーバーの環境構築が挙げられます。
色々なプロジェクトでは言語や依存するサーバー環境が異なります。これを解決するのは非常に面倒です。
そこで、 Docker を裏で利用してコンテナ内ですべて実行することにしました。
個人の意見として、ビルドやテストの実行環境はCIサーバーで担保するのではなく、プロジェクトで担保するべきだと思っています。
実行環境をプロジェクト毎に担保することによってCIサーバーのスケールや移行が楽になります。
コード内で Docker を利用する際には、
が非常に便利です。
これを利用するため、サーバーは Golang で記述することにしました。
また、ジョブ自体は Maven や Gradle、NPM、Fastlane などのタスクランナーを利用して実行することにしました。
サクッと利用方法を見たい場合は ここ から。
作る機能
CIの機能を分解すると、
- トリガー
- ジョブの実行
- 結果の通知
に分けられると思います。
トリガー
ジョブを実行するタイミングです。
今回はサーバーとして起動し、GitHub の Webhooks を待ち受けることにしました。
GitHub の Webhooks は種類・情報が豊富です。
Webhooks | GitHub Developer Guide
これを利用することで任意のタイミングでジョブを実行できるようになります。
GitHub の Pull Request のコメントをトリガーフレーズとして実行できるようにしました。
Jenkins の Pull Request Builder Plugin の trigger phrases と同じような使い勝手です。
ジョブの実行
前述のとおり、ジョブは Docker コンテナ内で実行します。
プロジェクト毎に Dockerfile を用意し、 ENTRYPOINT
にタスクランナーのコマンドを設定しておくことで、トリガーレーズで任意のタスクを実行できるようになります。
上記のような Dockerfile を用意していた場合、 ci test
とコメントすると コンテナ内で mvn test
が実行されます。
結果の通知
ジョブを実行したら、その結果を通知する必要があります。
リクエストに対して同期的にジョブを実行し、成功したら 200
を返すようにしました。
※ジョブに時間がかかってしまうので、GitHub の Webhooks は Timeout となってしまいます。
また、トリガーを GitHub の Pull Request 依存にしたので、Commit Status
として結果を返すことにしました。
コード
前置きが長くなりましたが、 以下が300行弱で簡易CIを実現するコードです。
ここからは、上から順に各ポイントを紹介したいと思います。
Webhooks のパース
net/http
を利用して Webhooks を待ち受けるサーバーを実現しています。
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { ... }) http.ListenAndServe(":8080", nil)
リクエストは JSON文字列 として受け取ります。
そのままでは非常に扱いづらいので、構造体にマッピングしましょう。
ただし、 Payload の構造体を自分で記述するのは骨が折れます。
以下のプロジェクトから構造体を拝借しました。
body, _ := ioutil.ReadAll(r.Body) ... event := &github.IssueCommentEvent{} json.Unmarshal(body, event)
ここでは、 github.IssueCommentEvent
という構造体にマッピングしています。
冒頭で記述したとおり、GitHub の Webhooks には非常に多くの種類(event type
)があります。
その種類によって Payload が異なるので、正しく構造体にマッピングしなければなりません。
どの event type
かは、リクエストのヘッダー X-GitHub-Event
に記述されています。
Pull Request のコメントは issue_comment
です。それ以外の場合は除外しましょう。
https://developer.github.com/v3/activity/events/types/#issuecommentevent
githubEvent := r.Header.Get("X-GitHub-Event") if githubEvent != "issue_comment" { ... return }
トリガーフレーズの取得
Pull Requestのコメント内容は Payload の comment.body
から取得することができます。
正規表現で /^ci\s+(?<phrase>.+)/
にマッチした場合にジョブを実行します。
if !regexp.MustCompile("^ci\\s+[^\\s]+").Match([]byte(event.Comment.GetBody())) { ... return } phrase := regexp.MustCompile("^ci\\s+").ReplaceAllString(event.Comment.GetBody(), "")
Pull Requestかどうかの判定
event type issue_comment
は名前から分かる通り Issue のコメントでも発火してしまいます。
Payload の issue.number
から得られた番号から、 Pull Request 情報を取得することができます。
pr, _, err := githubClient.PullRequests.Get( context.Background(), event.Repo.Owner.GetLogin(), event.Repo.GetName(), event.Issue.GetNumber(), )
特定のブランチを Clone する
Gitリポジトリの Clone には、
を利用しました。
特定のブランチをクローンする場合は CloneOptions.ReferenceName
を指定します。
refs/heads/<ブランチ名>
で指定する必要があります。
Pull Request の Head ブランチは上記で取得した Pull Request 情報から取得することができます。
base := fmt.Sprintf("%v", time.Now().Unix()) root := fmt.Sprintf("/tmp/%s", base) repo, err := git.PlainClone(root, false, &git.CloneOptions{ URL: event.Repo.GetCloneURL(), Progress: os.Stdout, ReferenceName: plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", pr.Head.GetRef())), })
GitHub に Pending の commit status をつける
Commit Status は進行によって更新するので、共通の処理は CommitStatusService
としてまとめておきました。
https://gist.github.com/duck8823/261fafdcea18b655ce6a49381499d9b5#file-main-go-L262-L294
git.PlainClone
した際の戻り値として得られる git.Repository
から、 HEAD の Hash を得ることができます。
Commit Status は特定の Commit Hash に対して設定することができます。
また、 Context
はトリガーフレーズごとに異なるようにしました。これにより build
は成功したけど test
に失敗したなどの情報がわかるようになります。
ref, err := repo.Head()
...
statusService := &CommitStatusService{
Context: fmt.Sprintf("minimal_ci-%s", phrase),
GithubClient: githubClient,
Repo: event.Repo,
Hash: ref.Hash(),
}
statusService.Create(PENDING)
Dockerイメージをビルドするための tarアーカイブ を作成する
Moby では Dockerfile を含むディレクトリを tar形式 でアーカイブする必要があります。
filepath.Walk
を利用して、クローンしてきたディレクトリに対して再帰的に tarアーカイブに含めるようにしています。
filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } file, _ := os.Open(path) defer file.Close() data, _ := ioutil.ReadAll(file) header := &tar.Header{ Name: strings.Replace(file.Name(), root, "", -1), Mode: 0666, Size: info.Size(), } writer.WriteHeader(header) writer.Write(data) return nil })
イメージのビルド
client.NewEnvClient()
で DOCKER_HOST
などの環境変数を利用した Dockerクライアント を作成することができます。
Client.ImageBuild
では非同期にDocker Imageのビルドが実行されます。
戻り値である types.ImageBuildResponse
の Body
を EOF
まで読むことでビルドの終了を同期的に待つことができます。
また、types.ImageBuildOptions
の Tags
で タグを指定することができます( -t
オプション相当 )。
タグにはコンテナの作成時と同じものを利用しましょう。今回は 実行ごとに UNIX時間 をつけているのですが、
リポジトリ名などでキャッシュを利用するようにすれば、ジョブの実行が高速化されるかもしれません。
cli, _ := client.NewEnvClient() resp, _ := cli.ImageBuild(context.Background(), file, types.ImageBuildOptions{ Tags: []string{base}, }) defer resp.Body.Close() ioutil.ReadAll(resp.Body) logger.Info("Image Build succeeded.")
コンテナの作成
コンテナを作成します。
Cmd に トリガーフレーズ 指定することで、任意のタスクの実行を実現できます。
con, _ := cli.ContainerCreate(context.Background(), &container.Config{ Image: base, Cmd: []string{phrase}, }, nil, nil, "")
コンテナの実行
いよいよコンテナの実行です。 コンテナ作成時に取得した コンテナID をスタートさせます。
コンテナの実行は同期的に行われます。
Client.ContainerWait
で待つことができます。戻り値には コンテナ内で実行したコマンドの return code
が含まれます。
return code
が 0
以外のときはジョブに失敗したとみなし、 Commit Status を failure
にしました。
また、ログは Client.ContainerLogs
で取得できます。
cli.ContainerStart(context.Background(), con.ID, types.ContainerStartOptions{}) if code, err := cli.ContainerWait(context.Background(), con.ID); err != nil { ... return } else if code != 0 { statusService.Create(FAILURE) http.Error(w, fmt.Sprintf("return code: %v", code), http.StatusInternalServerError) return } out, _ := cli.ContainerLogs(context.Background(), con.ID, types.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, })
成功の通知
最後までエラーが無ければ、 Commit Status を success
にしましょう。
また、レスポンスにログを渡しています。
statusService.Create(SUCCESS) buf := new(bytes.Buffer) buf.ReadFrom(out) respBody, err := json.Marshal(struct { Console string `json:"console"` }{ Console: buf.String(), }) w.WriteHeader(http.StatusOK) w.Write(respBody)
使い方
準備
Commit Status を作成するために、 GITHUB_API_TOKEN
を設定する必要があります。
トークンは https://github.com/settings/tokens から取得することができます。
export GITHUB_API_TOKEN=<your api token>
サーバーを起動する
Go の環境がある場合
$GOPATH が設定されていて、 $GOPATH/bin
に $PATH が通っている場合、は go get
で取得できます。
go get -u github.com/duck8823/minimal-ci minimal-ci
Docker Compose で起動する (コンテナからホストのDockerデーモンを利用する)
サーバー自体を Docker コンテナで起動する場合も、ホストの Docker デーモンを利用して実行するようにしましょう。
OS によって若干異なるのでそれぞれの compose ファイルを用意しました。
Windows の場合
Docker for Windows を利用します。
あらかじめ Docker for Windows の Settings から、
Expose daemon on tcp://localhost:2375 without TLS
を設定しておきましょう。
DOCKER_HOST
として tcp://docker.for.win.host.internal:2375
を設定することでコンテナ内からホストの Dockerデーモン を利用することができます。
docker-compose -f docker-compose.win.yml
Mac の場合
Docker for Mac を利用します。
Docker for Mac には Expose daemon...
の設定はありません。
bobrik/socat
のイメージを利用することで実現できました。
https://hub.docker.com/r/bobrik/socat/
また、 DOCKER_HOST
は tcp://docker.for.mac.host.internal:2375
です。
docker-compose -f docker-compose.mac.yml
Optional.of( ngrok の利用 )
Webhooks を受け取るために、GitHubからサーバーにアクセスできる必要があります。
ngrok でローカルのポートを外部に公開することができます。
試しにポートを公開したい場合は利用するといいかもしれません。
※リポジトリの composeファイル では ngrok コンテナはコメントアウトしています。
ngrok を起動したら http://localhost:4040/status
にアクセスしてみましょう。
ここで表示される URL にアクセスすることによって、 フォワーディングされます。
Webhooks の設定
GitHubでは、リポジトリ毎に Webhooks を設定します。
https://github.com/<owner>/<repo>/settings/hooks/new
Payload URL には 公開しているURL を設定します。
Which events would you like to trigger this webhook?
には Issue comments
を設定しましょう。
まとめ
GitHub / Docker( Moby ) を使って簡易CIを作ってみました。
セキュリティがガバだったり、失敗時のログを後から見られなかったりとまだまだ機能は足りないですが、
このコードをベースにオリジナルのCIを作ってみると楽しいと思います。