Go+Webアプリケーション+CircleCIで静的解析・テスト・バイナリリリースを効率良く行なう

この記事は[CircleCI Advent Calendar 2015](http://qiita.com/advent-calendar/2015/circleci)の5日目の記事です。

## はじめに

Go+Web+CIの実戦的な話です。Go+CIに関してだと色々な手法は出てきていますが「実際どうすればいいの？」と感じている方が多いと思います。
また、昨今Go+Webアプリも増えているので、これからGoでWebアプリケーションを作ろうと思っていた方の継続的インテグレーションへのアプローチの足掛かりとなれば嬉しいです。

### （Go要素強いです）

＿人人人人人人人人人人人人人人人人人人人人人人人人人人人＿
＞　本当は12/6の[GoConference](http://eventdots.jp/event/573121)で話そうと思ってたんだぜ!!　＜
￣ＹＹＹＹＹＹＹＹＹＹＹＹＹＹＹＹＹＹＹＹＹＹＹＹＹＹＹ￣

※ GoConのセッション外れた＋抽選も外れた勢

さて、今回使用したリポジトリは下記にあります。
Base64 Encode/Decodeするだけですが、Go+Web+CIサンプルとしても使えるので是非。

- [kaneshin/base64server](https://github.com/kaneshin/base64server)
- [kaneshin/base64server/circle.yml](https://github.com/kaneshin/base64server/blob/master/circle.yml)


## やっていること

circle.ymlに沿ってすすめるのが難しいので、やっていることベースで話を進めたいと思います。circle.ymlはあっちいったりこっちいったりしますので、ご了承ください。

- Goインストール
- goxを用いてCross-Compileし、ghrを用いてGitHubへリリース
- 実行ファイルにビルドバージョンとビルド日時をセットする
- 静的解析: `gofmt`, `go vet`, `golint`
- Request Collection Runner （広く呼ばれる名前を知らない…）

### ◆ Goインストール

#### yaml

```yaml
machine:
  pre:
    - >
      curl -o go.tar.gz -sL https://storage.googleapis.com/golang/go1.5.2.linux-amd64.tar.gz &&
      sudo rm -rf /usr/local/go &&
      sudo tar -C /usr/local -xzf go.tar.gz &&
      sudo chmod a+w /usr/local/go/src/
```

自分でGoをインストールしています。ただ、それだけです。

[moovweb/gvm](https://github.com/moovweb/gvm)を使うのもアリだと思います。が、ちゃちゃっとやりたいときや、pre-releaseのものを使用したい場合が出てくる可能性もあるので、自分でインストールしてしまう方が速いです。Goに限って言えばGoのインストールは超楽なので。

- Go公式が配布しているパッケージは[こちら](https://golang.org/dl/)にあります。

### ◆ goxを用いてCross-Compileし、ghrを用いてGitHubへリリース

これはGo+CIなら有名だと思います。が、一応紹介しようと思います。

#### - [mitchellh/gox](https://github.com/mitchellh/gox) -

Goは基本的に稼働しているOSに対応する実行ファイルを作成しますが、ターゲットを指定するとクロスコンパイルすることが可能です。
クロスコンパイルは非常に簡単ですが、知らないと手こずるのでgoxを使用するのが吉かもしれません。

- [クロスコンパイルについて (GeekSalon#7) by Shintaro Kaneko](https://speakerdeck.com/kaneshin/kurosukonpairunituite-geeksalon-number-7)

#### - [tcnksm/ghr](https://github.com/tcnksm/ghr) -

ghrは[高速に自作パッケージをGithubにリリースするghrというツールをつくった](http://deeeet.com/writing/2014/07/29/ghr/)が作者ページなのでこちらを一読ください。

#### yaml

```yaml
deployment:
  release:
    branch: master
    commands:
      - go get github.com/mitchellh/gox
      - go get github.com/tcnksm/ghr
      - gox -ldflags "-X main.BuildVersion $BUILD_VERSION -X main.BuildDate $BUILD_DATE" -output "dist/${CIRCLE_PROJECT_REPONAME}_{{.OS}}_{{.Arch}}"
      - ghr -t $GITHUB_TOKEN -u $CIRCLE_PROJECT_USERNAME -r $CIRCLE_PROJECT_REPONAME --replace `git describe --tags` dist/
```

使用する `gox` と `ghr` を `go get` し、まずは `gox` でビルドします。ビルドして作成される実行ファイルは `dist` ディレクトリに格納しています。

#### $CIRCLE_PROJECT_USERNAME, $CIRCLE_PROJECT_REPONAME ?

CircleCIでは、コンテナを起動する際（`Start container`）にリポジトリとユーザの情報を環境変数に登録してくれます。

![Screen Shot 2015-12-05 at 6.11.19 PM.png](https://qiita-image-store.s3.amazonaws.com/0/3353/a131bbf7-e0ba-93a5-4610-9463b8359e4e.png)


時折、いや、結構いる気がするのですが、このCircleCIが設定してくれる環境変数を知らない方がいます。
これないとマジックナンバーばかりのcircle.ymlとなってしまう可能性もあるので気をつけて下さい。

#### **Tips**: `CIRCLE_COMPARE_URL` と `git log --name-only` を使って差分ファイルを見つける

弊社のプロジェクトの場合、`CIRCLE_COMPARE_URL`を利用してハッシュ部分を取り出し、前回と今回で変更点があったファイルに関係するテストを走らせることをしています。

例えば、`git log --no-merges --name-only 9bb7a0a346ac...d32dc33ead2c`のようにして結果を取得し、`grep`して修正ファイルを見つけ出して、限定したテストを走らせることによってCircleCIを効率良く回すことをしています。

また、`CIRCLE_BRANCH`が`master`だった場合は**フルテスト**を行なうこともしています。

-------

話を戻しましょう。

次に `ghr` で `dist` に格納した実行ファイルをGitHubへリリースします。リリースするためには

- 1. GitHubの [Personal access tokens](https://github.com/settings/tokens) へ遷移する
- 2. `Generate new token` でトークンを作成する。（スコープは変更しなくてよい）
- 3. 作成されたトークンをコピーする
- 4. CircleCIの Project Settings > Tweaks > Environment variables へ遷移する
- 5. Nameを `GITHUB_TOKEN` にし、Valueに"3"でコピーしたトークンをペーストする

**注意：面倒だからといって、`GITHUB_TOKEN`はcircle.ymlに直書きしないよう気をつけて下さい。**

![Screen_Shot_2015-12-05_at_6_10_23_PM.png](https://qiita-image-store.s3.amazonaws.com/0/3353/c02b0bc4-3b8f-2e51-aace-e3077e24ebd5.png)

ここまですればcircle.ymlはコピペでOKです。

実行後に[リリースページ](https://github.com/kaneshin/base64server/releases)を参照すると

![Screen Shot 2015-12-05 at 6.13.25 PM.png](https://qiita-image-store.s3.amazonaws.com/0/3353/11333685-a358-b65c-52d5-a4e0032573e5.png)

のようになっています。

もし、実行ファイルがアップロードできていない場合はリリースタグを適当にひとつ設定してください。

### ◆ 実行ファイルにビルドバージョンとビルド日時をセットする

あるエンドポイントを叩くと、実行ファイルのバージョン（コミットハッシュ）とビルドした日時をわかるようにしています。Webアプリケーションを作成するなら**絶対**にあった方が良いです。

```sh
$ curl -XGET "localhost:8080/version"
Version: d32dc33-dirty
   Date: 2015-12-05T07:50:13+0000
```

このバージョン情報があるだけで、前回との差分を容易に調べることができます。

```sh
hash=`curl -XGET "localhost:8080/version" | (sedなどで出力を整形する) `;
git --git-dir=$GOPATH/src/github.com/kaneshin/base64server/.git log \
  --no-merges --oneline ${hash}...origin/master
```

これでリリースするコミットを簡単に得ることができます。弊社のプロジェクトではJSONフォーマットなので、出力の整形はそれに応じた形で対応しています。

さて、これの実装方法 (circle.yml) です。

#### yaml

```yaml
machine:
  environment:
    CHECKOUT_PATH: $HOME/$CIRCLE_PROJECT_REPONAME
  post:
    - >
      echo "export BUILD_VERSION=\"`git --git-dir=${CHECKOUT_PATH}/.git describe --always --dirty`\"" >> ~/.circlerc;
      echo "export BUILD_DATE=\"`date +%FT%T%z`\"" >> ~/.circlerc;

deployment:
  release:
    branch: master
    commands:
      - go get github.com/mitchellh/gox
      - go get github.com/tcnksm/ghr
      - gox -ldflags "-X main.BuildVersion $BUILD_VERSION -X main.BuildDate $BUILD_DATE" -output "dist/${CIRCLE_PROJECT_REPONAME}_{{.OS}}_{{.Arch}}"
      - ghr -t $GITHUB_TOKEN -u $CIRCLE_PROJECT_USERNAME -r $CIRCLE_PROJECT_REPONAME --replace `git describe --tags` dist/
```

先ほどの `gox` の部分でスルーしたのですが、 `gox` でビルドする際に `ldflags` オプションをつけています。
これは `go build` のオプションで、`-X`で指定した変数（今回だとmainパッケージの`BuildVersion` と `BuildDate`）に情報を設定することができます。

それぞれ、 

- `BuildVersion` には `$BUILD_VERSION`
- `BuoldDate` には `$BUILD_DATE`

を付与しており、それらの `$BUILD_{VERSION,DATE}`は`machine`セクションで定義しています。

```sh
echo "export BUILD_VERSION=\"`git --git-dir=${CHECKOUT_PATH}/.git describe --always --dirty`\"" >> ~/.circlerc;
echo "export BUILD_DATE=\"`date +%FT%T%z`\"" >> ~/.circlerc;
```

#### `~/.circlerc`

CircleCIでは変数を`export`しても次のステップでは無力化されています。
- 詳細：https://circleci.com/docs/environment-variables#custom

ですが、`~/.circlerc` というファイルが毎度のステップで読み込みをしてくれるため、ここでexportを定義すれば `machine.environment`と同じ効果を得ることができます。

### ◆ 静的解析: `gofmt`, `go vet`, `golint`

Goの静的解析で失敗すればエラーとなるようにします。これで綺麗なプロジェクトを保つことができます。

静的解析には

- gofmt
- go vet
- golint
- goveralls

が有名でしょう。今回、goverallsは省きますが、絶対にやったほうがいいです。カバレッジはテスト駆動開発のひとつの指標となるためです。

（カバレッジはテストを書くモチベの源泉である - 参考：[XcodeCoverageについて](https://speakerdeck.com/kaneshin/xcodecoveragenituite?slide=24)）

#### yaml

```yaml
test:
  override:
    - test -z "$(gofmt -s -l . | tee /dev/stderr)"
    - go vet ./...
    - test -z "$(golint ./... | tee /dev/stderr)"
    - go test -race -test.v ./...
```

#### gofmt

```sh
test -z "$(gofmt -s -l . | tee /dev/stderr)"
```

gofmtを行い、フォーマットされていないファイル名を出力します。`test -z` で文字があるなら `false` が返却されて、CircleCIが失敗となります。

#### go vet

```sh
go vet ./...
```

`struct`につけるタグ（アノテーション）を解析してくれます。
`go build`ではスルーされてしまうので、仮に違反したタグが存在したとしてもそのままビルドが完了し、期待した結果が返ってこなくてめっちゃハマるでしょうね…（アノテーションでハマる人はたくさんいる）

#### golint

```sh
test -z "$(golint ./... | tee /dev/stderr)"
```

GoのLintです。コメント等なければ下記のようにその旨を返却してきます。

```
main.go:13:2: exported var BuildVersion should have comment or be unexported
```

エクスポータブルな変数にコメントがないと絶対エラーになるので、コメントの強制力がハンパないです。


### ◆ Request Collection Runner （広く呼ばれる名前を知らない…）

#### yaml

```yaml
test:
  pre:
    - go version
    - go build -a -v -o /tmp/base64server .
    - /tmp/base64server -port=${APP_PORT}:
        background: true
  override:
    - ./runner.sh
```

実際に実行ファイルを起動して、意図しているリクエストをサーバが受け付けているかを確認します。

一般的な名前がわからないのですが、Postmanにある機能名をパクりました

- [Postman - Running a collection](https://www.getpostman.com/docs/jetpacks_running_collections)

#### runner.sh

`curl --fail` で叩いて、exitコードを見ているだけです。

```bash
#!/bin/bash

if [[ -z "${APP_PORT}" ]]; then
  APP_PORT="8080"
fi

function __check()
{
  req="localhost:${APP_PORT}/${1}";
  curl --fail -XGET "${req}"
  ret=$?
  if [[ ! $ret = "0" ]]; then
    echo "ERROR: ${req}" > /dev/stderr
    exit $ret
  fi
}

# version
__check "version"

# encode
__check "encode?v=hello+world";

# decode
__check "decode?v=aGVsbG8gd29ybGQ=";
```

APIの実装とかでroutingをぶっ壊す人が稀にいますからね。（弊社にはいません）


## おわりに

すごく長くなってしまいました…（こんなはずでは…）
Go+CIでは他に `goveralls`も話したかったのですが、なかなか長くなってしまったので今回は省きました。

そして、、12/5の本日、たまたまCircleCIのUIが変わったせいで、スクリーンショットを全て作り直しましたとさ…