CircleCIを使ってbundle updateを定期実行する

# 自動実行の流れ

![a0c0b578-7cf8-2a98-0bf5-ccd21005aa8e.png](https://qiita-image-store.s3.amazonaws.com/0/153/b961682a-060b-b752-1e77-f9155305d0a7.png)



## 1. CircleCIを起動する

[Nightly builds](https://circleci.com/docs/nightly-builds) というAPIを使います。本来は重たいテストを夜中に1回だけ実行する、みたいな用途で使うことを意図しているのでNightly buildsという名前になっていますが、簡単な話が「外から環境変数を追加した上で、特定のブランチに対するテストを実行させる」機能です。 

`BUNDLE_UPDATE` という環境変数を設定した上でmasterブランチをビルドすると`bundle update`が実行されるようにすることにしました。以下のようなスクリプトを用意し、cronで平日の朝に自動実行するようにしています。

```bash
_project=user-name/repo-name  # 適当に変える
_branch=master
_circle_token=$1

trigger_build_url=https://circleci.com/api/v1/project/${_project}/tree/${_branch}?circle-token=${_circle_token}

# BUNDLE_UPDATE環境変数をつけてCIを走らせる
post_data='{ "build_parameters": { "BUNDLE_UPDATE": "true" } }'

# Nightly buildsを実行
curl \
  --header "Accept: application/json" \
  --header "Content-Type: application/json" \
  --data "${post_data}" \
  --request POST ${trigger_build_url}
```

## 2. CIサーバ上で `bundle update` を実行する

`BUNDLE_UPDATE` かつmasterブランチの場合は `bundle update` だけを実行するようにします。[^1]

```yaml:circle.yml
test:
  post:
    - >
      if [ -n "${BUNDLE_UPDATE}" -a "${CIRCLE_BRANCH}" = 'master' ] ; then
        bundle update
      fi
```

[^1]: テストの実行もスキップするようにしています。

## 3. Gemfile.lockが変更されている場合はプルリクエストを作る

```yaml:circle.yml
deployment:
  auto-bundle-update:
    branch: master
    commands:
      - >
        if [ -n "${BUNDLE_UPDATE}" ] ; then
          bash script/circleci/create_pull_request_if_needed.sh
        fi
```

```bash:script/circleci/create_pull_request_if_needed.sh
export BRANCH=bundle-update-`date -u "+%Y%m%d"`
if [ -n `git status -sb 2> /dev/null | grep Gemfile.lock` ] ; then
  git config --global user.email user@eample.com
  git config --global user.name 'your name'
  git add Gemfile.lock
  git commit -m 'Bundle update'
  git branch -M $BRANCH
  git push origin $BRANCH
  bundle exec ruby script/circleci/create_pull_request.rb
fi
```

プルリクエストを作る部分はrubyで作りました。

```ruby:script/circleci/create_pull_request.rb
require 'octokit'

client = Octokit::Client.new(access_token: ENV['GITHUB_ACCESS_TOKEN'])
client.create_pull_request(
  'user-name/repo-name',  # 適当に変える
  'master',
  ENV['BRANCH'],
  'Bundle update',  # Title
  ''                # Body
)
```

## 4. githubのhookでCircleCIが起動され新しく作られたブランチのテストが走る

ここからは通常のCIと同じです。テスト結果に応じて3で作られたプルリクエストを煮るなり焼くなりします。

# その他の話題

## 背景

### bundle updateとは

`bundle update` は依存しているgemに新しいバージョンが公開されている場合、その新しいものを使うように更新する作業のこと。bundlerは明示的に `bundle update` を実行しなければ、同じgemの同じバージョンをずっと使い続ける。

### なぜupdateする必要があるのか

`bundle update` せずに放置すると、古いバージョンに含まれるバグ :bug: の修正や、パフォーマンスの改善、機能追加などの恩恵を享受することができない。逆に言えば、その果実を回収するために `bundle update` を実行する。

### なぜ定期実行する必要があるのか

`bundle update` を実行すると、場合によってはアプリが意図した通りに動かなくなることがある。これは `bundle update` によって変更されるバージョンの差分が大きければ大きいほど発生しやすくなり、また確認すべきドキュメント、コミットの数が増えるため対応するコストが大きくなる。大量のgemが一度にアップデートされると、仮に問題が発生した時に、それがどのgemを更新したことによって発生するようになったのかを切り分けることが難しくなるという問題も発生する。

定期実行すれば、大きなジャンプを経験する可能性が減るため、仮に問題が起きても対応するのが容易になる。

### なぜ自動化する必要があるのか

理由は単純に「忘れてしまう」ため。忘れてしまうと大きなバージョンチェンジにいっきに対応する必要が発生し、前述の通り追従にかかるコストが増加するのでますます `bundle update` する気がなくなってしまう悪循環が発生する。


## 関連実装

- [sanemat/tachikoma · GitHub](https://github.com/sanemat/tachikoma)

bundlerに限らず色々な言語のライブラリ管理ツールをアップデートするためのgem。抽象化しなくても十分に理解できるレベルだと思ったので使わなかった。

- [Dependency Update as a Service - Tachikoma.io](http://tachikoma.io/)

上のgemをSaaSとして提供しているもの。リポジトリへのアクセス権限を渡す必要があるので :no_good: publicリポジトリの自動更新をしたい場合は、これを使った方がいいと思う。