This is my life.

There are many like it, but this one is mine.

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 ブロックで secretsGITHUB_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 というツールを使うので、それっぽいアイコンにてみました。
f:id:duck8823:20181224123640p:plain

icon は 以下のものが利用できるようです。
feathericons.com

なお、 Danger では 各CI に対してクラスを定義する仕組みなので、上記 Pull Request 番号の取得方法などを踏まえて PR を出しました。既にマージしていただいているので、利用することができます。

github.com

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 が作成したことになっています。

f:id:duck8823:20181224131308p:plain

出来上がったワークフロー

完成したワークフローは以下のとおりです。かなりシンプルに表現できていると思います。 対象リポジトリで行うことは、下記ワークフローの定義と、実際にチェックをするための Dangerfile の作成になります。

workflow "Check Pull Request" {
  resolves = ["Danger"]
  on = "pull_request"
}
 
action "Danger" {
  uses = "duck8823/actions/danger@master"
  secrets = ["GITHUB_TOKEN"]
}

まとめ

おまけ

さて、最初に紹介したとおり pull_request をトリガーにするとレビューワーの追加などでもワークフローが実行されます。 CommitStatus はそれぞれ別に作成するようで...

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 で実行環境を定義するという方針で開発をしています。

さて、この DockerfileGitHub Actions で実行すれば、 Push の度にテストを実行してくれるということになりますね。

設定したいリポジトリActions タブをクリックすると、GUIで編集できる画面になります。
f:id:duck8823:20181220004237p:plain

Create a new workflow をクリックして編集してみましょう。 直感的に操作できるようになっています。
f:id:duck8823:20181220004539p:plain

最初からあるのはトリガーです。今はパブリックリポジトリでは pushしか選べません(2018/12/20 現在)。 f:id:duck8823:20181220004940p:plain 本能に従いトリガーの青い部分をドラッグ&ドロップすると、 Action を定義する箇所が現れます。

ここで、Dockerfile が存在するディレクトリの相対パスを指定することで、リポジトリ内の Dockerfile を利用することができます。
.duci/Dockerfile を実行したいとすると、 ./.duci/ となります。
f:id:duck8823:20181220005457p:plain

use をクリックすると更に詳細を設定できます。
GitHubの各リポジトリではSecrets が登録できるようになっており、ここで指定することでコンテナに渡すことができます。
f:id:duck8823:20181220005710p:plain

また、DockerflleENTRYPOINTCMD を上書きもできるようです。
ここではシンプルに何も上書き・設定しません。

GUI で編集したものは、定義ファイルとして保存されることになります。

workflow "New workflow" {
  on = "push"
  resolves = ["./duci/"]
}

action "./duci/" {
  uses = "./duci/"
}

なにやら見慣れない形式ですね。

リポジトリ.github/main.workflow というファイルが作成され、GUI上からコミットやブランチ&PRの作成までできてしまいます。
f:id:duck8823:20181220010322p:plain

これをコミットして実際に push すると、 Commit Status もつけてくれます。
f:id:duck8823:20181220010547p:plain

また、 Commit Status の details や Actions の log から、実行時のログを見ることもできます。
f:id:duck8823:20181220010720p:plain

本来、色々な Action を組み合わせて workflow を組み立てていくものですが、ここでは push の度にテストを回す方法を紹介しました。
テスト実行用の Dockerfile を用意してGitHub上でちょっと操作するだけで CIでテストが回せる時代になってしまいましたね。
書いたテストはガンガン実行していきましょう。

Spring Boot WebアプリケーションをCI上でスモークテストする

機能も作り込んだ。テストもオールグリーン。さあ張り切ってみんなの前でデモするぞ...

$ ./gradlew bootRun

...

エラー

ってこと、ありませんか?
僕はありました。

できるだけ実行コストをかけないように、ユニットテストではSpringに依存しないようにしています。
もちろん結合テストなどでは @SpringBootTest を利用していますがアプリケーション起動時のエラーは確認できません。

そんな中、以前 FlywayWebアプリケーションを起動しないで 実行する方法を調べていたことを思いました。

dev.classmethod.jp

※上記の記事時点では 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 を作っている話

前回の記事 で CI サーバーを作った話をしました。

ペライチ300行だったコードはコミットを重ね、少しずつ大きくなってきています。

github.com

リポジトリの名前も minimal-ci から duci に変えました。命名はお世話になっている先輩です。

f:id:duck8823:20180901004830g:plain

コンセプト

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の場合はそれ自体のサブコマンドとして taskbuild が用意されています。

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サーバーのようなものを作ることにしました。

github.com

CIサーバーを運用する際の悩みとして、サーバーの環境構築が挙げられます。
色々なプロジェクトでは言語や依存するサーバー環境が異なります。これを解決するのは非常に面倒です。
そこで、 Docker を裏で利用してコンテナ内ですべて実行することにしました。

個人の意見として、ビルドやテストの実行環境はCIサーバーで担保するのではなく、プロジェクトで担保するべきだと思っています。
実行環境をプロジェクト毎に担保することによってCIサーバーのスケールや移行が楽になります。

コード内で Docker を利用する際には、

github.com

が非常に便利です。

これを利用するため、サーバーは Golang で記述することにしました。

また、ジョブ自体は Maven や Gradle、NPM、Fastlane などのタスクランナーを利用して実行することにしました。
サクッと利用方法を見たい場合は ここ から。

作る機能

CIの機能を分解すると、

  • トリガー
  • ジョブの実行
  • 結果の通知

に分けられると思います。

トリガー

ジョブを実行するタイミングです。
今回はサーバーとして起動し、GitHub の Webhooks を待ち受けることにしました。
GitHub の Webhooks は種類・情報が豊富です。

Webhooks | GitHub Developer Guide

これを利用することで任意のタイミングでジョブを実行できるようになります。

GitHub の Pull Request のコメントをトリガーフレーズとして実行できるようにしました。
Jenkins の Pull Request Builder Plugin の trigger phrases と同じような使い勝手です。

f:id:duck8823:20180506115916p:plain

ジョブの実行

前述のとおり、ジョブは Docker コンテナ内で実行します。
プロジェクト毎に Dockerfile を用意し、 ENTRYPOINT にタスクランナーのコマンドを設定しておくことで、トリガーレーズで任意のタスクを実行できるようになります。

gist.github.com

上記のような Dockerfile を用意していた場合、 ci test とコメントすると コンテナ内で mvn test が実行されます。

結果の通知

ジョブを実行したら、その結果を通知する必要があります。
リクエストに対して同期的にジョブを実行し、成功したら 200 を返すようにしました。 ※ジョブに時間がかかってしまうので、GitHub の Webhooks は Timeout となってしまいます。

また、トリガーを GitHub の Pull Request 依存にしたので、Commit Status として結果を返すことにしました。

f:id:duck8823:20180506121019p:plain

コード

前置きが長くなりましたが、 以下が300行弱で簡易CIを実現するコードです。

gist.github.com

ここからは、上から順に各ポイントを紹介したいと思います。

Webhooks のパース

net/http を利用して Webhooks を待ち受けるサーバーを実現しています。

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    ...
})
http.ListenAndServe(":8080", nil)

リクエストは JSON文字列 として受け取ります。
そのままでは非常に扱いづらいので、構造体にマッピングしましょう。

ただし、 Payload の構造体を自分で記述するのは骨が折れます。
以下のプロジェクトから構造体を拝借しました。

github.com

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 には、

github.com

を利用しました。
特定のブランチをクローンする場合は 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.ImageBuildResponseBodyEOF まで読むことでビルドの終了を同期的に待つことができます。

また、types.ImageBuildOptionsTags で タグを指定することができます( -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 code0 以外のときはジョブに失敗したとみなし、 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 を利用します。

www.docker.com

あらかじめ Docker for Windows の Settings から、 Expose daemon on tcp://localhost:2375 without TLS を設定しておきましょう。

f:id:duck8823:20180506165017p:plain

DOCKER_HOST として tcp://docker.for.win.host.internal:2375 を設定することでコンテナ内からホストの Dockerデーモン を利用することができます。

docker-compose -f docker-compose.win.yml

Mac の場合

Docker for Mac を利用します。

www.docker.com

Docker for Mac には Expose daemon... の設定はありません。

bobrik/socat のイメージを利用することで実現できました。
https://hub.docker.com/r/bobrik/socat/

また、 DOCKER_HOSTtcp://docker.for.mac.host.internal:2375 です。

docker-compose -f docker-compose.mac.yml

Optional.of( ngrok の利用 )

Webhooks を受け取るために、GitHubからサーバーにアクセスできる必要があります。
ngrok でローカルのポートを外部に公開することができます。
試しにポートを公開したい場合は利用するといいかもしれません。
リポジトリの composeファイル では ngrok コンテナはコメントアウトしています。

ngrok.com

ngrok を起動したら http://localhost:4040/status にアクセスしてみましょう。

f:id:duck8823:20180506172407p:plain

ここで表示される 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 を設定しましょう。

f:id:duck8823:20180506172812p:plain

まとめ

GitHub / Docker( Moby ) を使って簡易CIを作ってみました。
セキュリティがガバだったり、失敗時のログを後から見られなかったりとまだまだ機能は足りないですが、 このコードをベースにオリジナルのCIを作ってみると楽しいと思います。