This is my life.

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

GitHub Actions のワークフローで Go 1.11 Modules のキャッシュを扱う

GitHub Actions で Golang の vet と test を実行したいと思います。
( ※実際には Go 1.10 からは go test の前に go vet が実行されますが、2つ以上のコマンドを実行したい場合を想定しています。)

ワークフローを以下のように定義しました。

workflow "vet and test" {
  on = "push"
  resolves = [
    "vet",
    "test"
  ]
}

action "vet" {
  uses = "docker://golang:1.11"
  runs = "go"
  args = ["vet", "./..."]
}

action "test" {
  uses = "docker://golang:1.11"
  runs = "go"
  args = ["test", "./..."]
}

f:id:duck8823:20181231085723p:plain

GitHub Actions では /github/workspaceワークスペースとなります。
また、使用している golang の公式Dockerイメージでは $GOPATH/go に設定されます。
Go 1.11 では $GOPATH の外では Modules (vgo)が利用されるので、 go vet および go test のそれぞれで依存モジュールのダウンロードを行いました。
同じモジュールを二度もダウンロードする必要はありません。一度だけダウンロードするようにしてみましょう。

Modules のキャッシュは $GOPATH/pkg/mod/cache 以下に配置されるため、そのままでは GitHub Actions のアクション間でキャッシュを共有することはできません。
ですが、少し工夫をすることでこれを使い回すことができます。

vendor ディレクトリを利用する方法 (失敗)

go mod vendor は依存するモジュールをダウンロードして vendorディレクトリ に配置してくれます。
GO111MODULE=on の状態で vendorディレクトリ を作成し、次のアクションでは GO111MODULE=off にすることで、 vendorディレクトリを利用することができます。

が、 GO111MODULE=off にした場合、( インポートを github.com から始めていると )自身のパッケージが解決できず、 失敗してしまいます。
無理やりならできるかもしれませんが、vendor は使わない方法にしました。

ワークスペース以下に $GOPATH を設定する方法

ワークスペース以下に作られたファイルやディレクトリは、次のアクションに引き継がれます。
これを利用し、 $GOPATHワークスペース以下に設定することで Module のキャッシュも渡してやることができます。
環境変数actionenv で設定することができます。
出来上がったワークフローは以下の通りです。

workflow "vet and test" {
  on = "push"
  resolves = [
    "vet",
    "test"
  ]
}

action "download" {
  uses = "docker://golang:latest"
  env = {
    GOPATH = "/github/workspace/.go"
  }
  runs = "go"
  args = ["mod", "download"]
}

action "vet" {
  uses = "docker://golang:latest"
  needs = ["download"]
  env = {
    GOPATH = "/github/workspace/.go"
  }
  runs = "go"
  args = ["vet", "./..."]
}

action "test" {
  uses = "docker://golang:latest"
  needs = ["download"]
  env = {
    GOPATH = "/github/workspace/.go"
  }
  runs = "go"
  args = ["test", "./..."]
}

f:id:duck8823:20181231085540p:plain

実行した場合の test のログは以下のようになりました。
ダウンロードのログは出ていませんね。

### STARTED test 21:31:58Z

Already have image (with digest): gcr.io/github-actions-images/action-runner:latest
?       github.com/duck8823/duci    [no test files]
ok      github.com/duck8823/duci/application    0.016s
?       github.com/duck8823/duci/application/cmd    [no test files]
ok      github.com/duck8823/duci/application/context    0.008s
ok      github.com/duck8823/duci/application/semaphore  0.010s
?       github.com/duck8823/duci/application/service/docker [no test files]
?       github.com/duck8823/duci/application/service/docker/mock_docker [no test files]
ok      github.com/duck8823/duci/application/service/git    0.045s
?       github.com/duck8823/duci/application/service/git/mock_git   [no test files]
ok      github.com/duck8823/duci/application/service/github 0.006s
?       github.com/duck8823/duci/application/service/github/mock_g
...

今回のケースでは、並列でダウンロードする場合もそんなに実行時間が変わるような変更はしていません。
ですが、ダウンロードは少なくして負荷をあまりかけないようにしたいですね。

まとめ

アクション間で引き継ぎたいディレクトリやファイルはワークスペース以下になるようにしましょう。

GitHub Actions で利用するDockerfileについて考える

最近 GitHub Actions を色々と試しています。 GitHub Actions で使う Dockerfile や対象リポジトリディレクトリ構成についてちょっと考えてみました。

DockerfileにおけるADD

GitHub Actions では、イベントの発生直後にDockerイメージのビルドを行うようです。
このとき、指定されたディレクトリの配下にあるファイルだけが ADD の対象になります。

以下のような構成のリポジトリーを想定してみましょう。

|-- hello-world (repository)
|   |__ .github
|       |__ main.workflow
|   |__ action
|       │__  Dockerfile
|-- Gemfile

このとき action/Dockerfile の内容を以下とします。

FROM ruby:2.6

ADD . .

RUN bundle install --path vendor/bundle

ENTRYPOINT ["bundle", "exec"]
CMD ["rspec"]

リポジトリー直下の Gemfile を使って、 Dockerイメージのビルド時に bundle install を行う想定です。
この構成で Action を実行すると、以下のようにエラーとなってしまいます。

...
Step 1/5 : FROM ruby:2.6
2.6: Pulling from library/ruby
Digest: sha256:4ec9d4622afc4abe11e282a9ee77c2edfa979263ba03075365007bad9bd67171
Status: Downloaded newer image for ruby:2.6
 ---> 91f360ea5325
Step 2/5 : ADD . .
 ---> 8ef90ba24305
Step 3/5 : RUN bundle install --path vendor/bundle
 ---> Running in a1abb0e2421a
Could not locate Gemfile
The command '/bin/sh -c bundle install --path vendor/bundle' returned a non-zero code: 10

### FAILED with bundle 03:35:35Z (11.064s)

Gemfile が存在しないと怒られてしまいました。
action ディレクトリー以下に Gemfile が存在しないためです。

いくつか回避する方法があります。

案1: Gemfile を action ディレクトリーに配置する

|-- hello-world (repository)
|   |__ .github
|       |__ main.workflow
|   |__ action
|       │__  Dockerfile
|       │__  Gemfile

この場合、bundle install は成功します。
しかし、普段の開発時に

bundle install --gemfile=action/Gemfile

のように Gemfile の場所を指定する必要が出てきます。 また、様々なツールなどがリポジトリー直下の Gemfile を想定して作られています。 それぞれのツールで Gemfileの場所 を設定する必要があるでしょう。

これを回避するように以下のように二重管理するのも個人的に好きではありません。

|-- hello-world (repository)
|   |__ .github
|       |__ main.workflow
|   |__ action
|       │__  Dockerfile
|       │__  Gemfile
|-- Gemfile

案2: 複数のアクションに分ける

ワークフローで bundle installbundle exec rspec を分けてしまう方法です。 この場合、 Dockerfile は以下のようになります。

FROM ruby:2.6

はい、そうですね。 Dockerfile を用意する必要はありません。
uses を docker://... として記述することで、 DockerHub のイメージを利用することができます。

|-- hello-world (repository)
|   |__ .github
|       |__ main.workflow
|-- Gemfile

ワークフローの定義は以下のようにします。

workflow "workflow" {
  on = "pull_request"
  resolves = [
    "test",
  ]
}

action "install" {
    uses = "docker://ruby:2.6"
    runs = "bundle"
    args = ["install", "--path", "vendor/bundle"]
}

action "test" {
    uses = "docker://ruby:2.6"
    needs = ["install"]
    runs = "bundle exec"
    args = "rspec"
}

2つのアクションでワークスペースは共有されており、 bundle exec rspec の前に bundle install を行うように定義することで実行することができます。
この方法で問題なさそうですが、アクションをいくつも定義することになり、ワークフローが複雑になるかもしれません。

案3: タスクランナーを action ディレクトリーに配置する

|-- hello-world (repository)
|   |__ .github
|       |__ main.workflow
|   |__ action
|       │__  Dockerfile
|       │__  Makefile
|-- Gemfile

公式などでは entrypoint.sh で紹介されていますが、ここではタスクランナーとして Makefile をおいています。
Makefile では以下のように2つのコマンドをまとめて定義しておきます。

test:
    bundle install --path vendor/bundle
    bundle exec rspec

ここで、 Dockerfile は以下のようになります。

FROM ruby:2.6

ADD Makefile /Makefile

ENTRYPOINT ["make", "-f", "/Makefile"]
CMD ["test"]

Makefile を ADD し、 コマンドでパスを指定しています。

アクション実行時に実際に作業するパスは /github/workspace ですが、 /github/workspace にファイルをADDしても実行時にはファイルが見当たりません(おそらくボリュームマウントしている?)。
よって、 /github/workspace ではない任意のディレクトリにADDしたあとにコマンドでパスを指定する必要があります。

この方法の場合、ワークフローで何をしているか知りたい場合に action ディレクトリ以下の Makefile の中を見る必要があります。

案4: タスクランナーをリポジトリルートに配置する

先程紹介した test タスク は、 CI だけでなく開発時にもよく使います。
スクランナー( Makefile )をリポジトリのルートに配置しましょう。
この場合も DockerHub のイメージで十分です。

|-- hello-world (repository)
|   |__ .github
|       |__ main.workflow
|-- Gemfile
|-- Makefile

ワークフローは以下のようになります。

workflow "workflow" {
  on = "pull_request"
  resolves = [
    "test",
  ]
}

action "test" {
    uses = "docker://ruby:2.6"
    runs = "make"
    args = "test"
}

かなりスッキリしました。 こうしておくと、別のCIサービスを利用している場合も make test だけでテストが実行できるようになります。

複数のタスクを実行する場合

bundle exec rspec および bundle exec danger など複数の bundle exec を利用したい場面を想定します。
アクションをバラバラに定義して都度 bundle install をするのは非効率的です。
この場合 ワークフローを以下のように定義することで、 install を一回にまとめることができます。

workflow "workflow" {
  on = "pull_request"
  resolves = [
    "test",
    "danger",
  ]
}

action "install" {
    uses = "docker://ruby:2.6"
    runs = "bundle"
    args = ["install", "--path", "vendor/bundle"]
}

action "test" {
    uses = "docker://ruby:2.6"
    needs = ["install"]
    runs = "bundle exec"
    args = "rspec"
}

action "danger" {
    uses = "docker://ruby:2.6"
    needs = ["install"]
    secrets = ["GITHUB_TOKEN"]
    runs = "bundle exec"
    args = "danger"
}

上記は 案2 の例です。

bundle install ではキャッシュが効くので、案4 の場合でも非効率的にはならないでしょう。
Makefileinstall タスクを追加して、test と danger のそれぞれのアクションを依存させます。

install:
    bundle install --path vendor/bundle

test:
    bundle install --path vendor/bundle
    bundle exec rspec

danger:
    bundle install --path vendor/bundle
    bundle exec danger

ワークフローは以下のようになります。

workflow "workflow" {
  on = "pull_request"
  resolves = [
    "test",
    "danger",
  ]
}

action "install" {
    uses = "docker://ruby:2.6"
    runs = "make"
    args = "install"
}

action "test" {
    uses = "docker://ruby:2.6"
    needs = ["install"]
    runs = "make"
    args = "test"
}

action "danger" {
    uses = "docker://ruby:2.6"
    needs = ["install"]
    secrets = ["GITHUB_TOKEN"]
    runs = "make"
    args = "danger"
}

この方法であれば、ローカル開発時は make test のみで依存のインストール(アップデート)から行ってくれ、CIでもキャッシュが効きます。便利ですね。

f:id:duck8823:20181231052737p:plain

じゃあいつ Dockerfile を使うの?

今回は GitHub Actions で利用する Dockerfile を考えているうちに、Dockerfile を使わなくなりました。 Dockerfile は必要なコマンドなどを追加でインストールする場合に利用します。
例えば以下のような場合です。

FROM ruby:2.6

ADD apt-get install git

git コマンドが必要なため、ベースイメージに追加しています。

まとめ

案2 または 案4 が良さそうだと思っています。
推しは 案4 です。Dockerfileには実行に必要な依存のみを記述( DockerHubにイメージがあるならイメージを指定 )し、タスクはリポジトリルートのタスクランナーに記述すると便利です。

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

以下の記事で簡単に紹介しています。
blog.duck8823.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コケるので便利でした。

一見目的が違うけど、「あー、あれ使ったらこれ達成できるなー。」って瞬間が好き。