CircleCIとdangerで、どんなプロジェクトでも簡単に「Stop saying "you forgot to …"」

[CircleCI Advent Calendar 2018](https://qiita.com/advent-calendar/2018/circleci)の9日目です🎄

8日目の記事は @f-naoto832  さんの[CircleCIで試行錯誤した話](https://qiita.com/f-naoto832/items/ec13f2f8db48927299b9)でした。

# はじめに

この記事は
[CircleCI2.0](https://circleci.com/)と[danger（ruby）](https://danger.systems/ruby/)専用のdocker imageを使うことで
どんなプロジェクトでもdangerを利用できるようにした例を紹介します。

# 主な対象読者

1. 最終的にPRで、approveする人（Techリードとか?）
2. ruby, js, swift以外のどんなプロジェクトでもdangerを実行したい人
3. CIの整備をしている人

# きっかけ

1. ソースコードレビュー時、「〇〇、まだ終わってないんだけど」って毎回指摘するのがめんどくさい。そして、される側も嫌だろう。 :sob:
2. ruby on railsのプロジェクトにはdangerをGemfileに追加しているが、Scala / Java / Golangとかのプロジェクトでもやりたい。 :sob:
3. モノリシックだったが、マイクロサービスにして、リポジトリが増えて、メンテしなければならないDangerfileが増えた。:sob:
4. dangerのバージョンを変更する時や、danger pluginを追加する時に、毎回それぞれのリポジトリのGemfileを変更しなければならない。 :sob:
5. そもそもサービスに関係ないdangerのgemをGemfileに追加するのはいかがなものだろうか？ :thinking: 

# 前提

1. danger自体の説明は他にもわかりやすい資料があるので、そちらを参考。  
ex. Dangerでpull requestレビューの指摘事項を減らす  
https://www.slideshare.net/ShunsukeMaeda/dangerpull-request
2. `Dangerfile`の詳細な書き方に関しては、触れません。
3. 「Only Build pull requests」関連の話は、別の記事を参考。  
ex. CircleCIでDangerを動かすためにOnly Build Pull Requestsの設定を調べてみた  
https://blog.sshn.me/posts/circleci-only-build-pull-requests/
4. docker image作成時の[circleciの設定](https://github.com/jkkitakita/circleci-danger/tree/master/.circleci)に関しても触れません。

# 作ったもの

| 対象 | URL |
|:-----------|:------------|
| circleci-danger | https://github.com/jkkitakita/circleci-danger |
| dangerfile | https://github.com/jkkitakita/dangerfile |
| dockerhub | https://hub.docker.com/r/jkkitakita/circleci-danger/ |
| circleci-danger-sample | https://github.com/jkkitakita/circleci-danger-sample |

# 各種設定

## Dockerfile

まずは、danger専用のdocker imageのDockerfile.
https://github.com/jkkitakita/circleci-danger/blob/master/Dockerfile

```Dockerfile
FROM ruby:2.3.1-alpine

ARG VCS_REF
ARG BUILD_DATE
LABEL maintainer="jkkitakita (Jun Kitamura)" \
      org.label-schema.url="http://jkkitakita.com" \
      org.label-schema.build-date=$BUILD_DATE \
      org.label-schema.vcs-url="https://github.com/jkkitakita/circleci-danger"\
      org.label-schema.vcs-ref=$VCS_REF

ENV HOME=/home/circleci

# CircleCIのuserを作成
RUN apk update && apk upgrade && \
  apk add --no-cache bash git openssh && \
  addgroup -g 3434 circleci && \
  adduser -D -u 3434 -G circleci -s /bin/bash circleci

# CircleCIのuserへ切り替え
USER circleci

# Gemfile, Gemfile.lock, Dangerfileをimage内にcopy
COPY --chown=circleci:circleci Gemfile* $HOME/danger/
COPY --chown=circleci:circleci Dangerfile $HOME/danger/

# $HOME/dangerへ移動
WORKDIR $HOME/danger

# bundlerをinstallして、globalにdangerをinstall
RUN gem install bundler --no-document && \
    bundle install

CMD ["/bin/sh"]
```

### ポイント
1. `circleci` userを作成しました。（rootじゃなくても、dangerだけ実行できればいいので。）
2. ciでの実行頻度が高い可能性があるので、なるべく軽くするために`alpine`を利用した。（2.4/2.５も試したのだが、うまくいかなかった。。なぜ。。）
3. `Gemfile`, `Gemfile.lock`, `Dangerfile`にcircleci user権限を付与しながらcopyしないと、権限がない。
4. 事前にしておけば、あとでbundle installする必要ない。

## Gemfile

bundle installする時のGemfile.
https://github.com/jkkitakita/circleci-danger/blob/master/Gemfile

```ruby
source 'https://rubygems.org'

gem 'danger'

# Add a danger plugin if necessary.
## gem 'danger-plugin_name'
```

### ポイント

1. `gem 'danger'`を追加
2. 必要に応じて、`gem 'danger-plugin_name'`を追加  （個人的には、LGTMのplugin好きです。笑）  

```ruby

gem 'danger-todoist'
gem 'danger-lgtm'
```

https://github.com/hanneskaeufler/danger-todoist
https://github.com/leonhartX/danger-lgtm


## Dangerfile（共通）

docker imageに含める共通Dangerfile.
https://github.com/jkkitakita/circleci-danger/blob/master/Dangerfile

```ruby
# Change to the path of the dangerfile you want to import.
danger.import_dangerfile(github: "jkkitakita/dangerfile", branch: "master")
```

### ポイント

1. `danger.import_dangerfile`を利用する  
https://danger.systems/reference.html
2. `Dangerfile`自体を、別リポジトリで管理するようにしておけば、毎回docker imageを作り直す必要がない
3. また、CircleCI実行時、このDangerfileを修正してから、dangerを実行するようにすれば、プロジェクト毎に違うルールで、CIを実行できる。

## dangerfile/Dangerfile（ex. ruby on rails）

`danger.import_dangerfile`するDangerfileの例。
https://github.com/jkkitakita/dangerfile

```ruby
# Alias
BIG_PULL_REQUEST_LINES = 1000
APP_FILES = /^(app|lib)/
TEST_FILES = /^(features|spec|test)/
has_app_changes = !git.modified_files.grep(APP_FILES).empty?
has_test_changes = !git.modified_files.grep(TEST_FILES).empty?

## Check label and body of PR.
has_label_wip = github.pr_title.match(/wip/i) || github.pr_labels.include?('wip') || github.pr_labels.include?('Wip') || github.pr_labels.include?('WIP')
has_pr_body_no_check = github.pr_body.match(/- \[ \]/)

# Request changes and Attention
## TODO in code.
## ref. https://github.com/hanneskaeufler/danger-todoist
todoist.message = "TODO対応してくれないの？:cry:"
todoist.warn_for_todos
todoist.print_todos_table

## WIP
fail('まだWIPだからmergeできないの！') if has_label_wip

## TODO in PR body.
fail('「[ ] チェックリスト」全部終わらせるなの！') if has_pr_body_no_check

## Attention to code
warn('1000行以上の修正は、危ないかもだけどー？') if git.lines_of_code > BIG_PULL_REQUEST_LINES
warn('appのコードを修正してるけど、testは書かないのー？') if has_app_changes && !has_test_changes

## Attention to a PR without any changes
if git.modified_files.empty? && git.added_files.empty? && git.deleted_files.empty?
  message('何も修正してないかもだけどー？')
end

# Procedure for re-executing CircleCI
message(
  "@#{github.pr_author}\n\n"\
  "失敗した時は、指摘内容を修正して\n"\
  "<a href='https://circleci.com/workflow-run/" + ENV['CIRCLE_WORKFLOW_ID'] + "'>https://circleci.com/workflow-run/" + ENV['CIRCLE_WORKFLOW_ID'] + "</a>\n"\
  "から「Rerun from failed」を実行してなの！"
) if !violation_report[:errors].empty?

# LGTM pic
# ref. https://github.com/leonhartX/danger-lgtm
lgtm.check_lgtm
```

### ポイント

1. `APP_FILES`、`TEST_FILES`あたりはプロジェクト毎に追加・修正
2. GitHubのPRの画面上に、CircleCIの「Trigger Jobs」を叩く「ボタン」をmessageに表示させたかったが、できなかったので、workflowのリンクを表示させた。

# .circle/config.yml（各リポジトリ側）
```yaml
version: 2

jobs:
  danger:
    docker:
      - image: jkkitakita/circleci-danger
    steps:
      # ~/projects（defaults）へ、対象リポジトリのsourceをcloseする
      - checkout

      # 共通Dangerfileをコピー
      # 対象リポジトリにすでにあるDangerfileを利用する場合、実行不要。
      - run: cp -f ~/danger/Dangerfile .

      # danger実行
      # --fail-on-errors=trueをつけると、PRでfailになります。
      - run: bundle exec danger --fail-on-errors=true

workflows:
  version: 2
  danger:
    jobs:
      - danger
      # contextを使う場合
      # - danger:
      #     context: danger
```

### ポイント

1. 共通Dangerfileを利用する場合、`~/danger/Dangerfile`をカレントへコピーすればok。
2. `--fail-on-errors=true`をつけて実行しないと、jobは成功しているけど、dangerだけ失敗してしまう（rerunできない）
3. 複数プロジェクトで利用する場合は、`context`にした方が、プロジェクト毎にEnvitonmentsを毎回設定しなくて良い。

# 実際にやってみると...

<img width="850" alt="ScreenShot 2018-12-09 16.45.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/95804/c44ce510-81a6-d779-b761-5ccdcb780478.png">

:tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: :tada: 

1. GitHub - PR  
https://github.com/jkkitakita/circleci-danger-sample/pull/1
2. CircleCI - Job  
https://circleci.com/gh/jkkitakita/circleci-danger-sample/8

# 結論

1. 業務では、`Scala / Java / Golang / js etc...`なプロジェクトにこのdanger jobを導入していますが、今の所、問題なく動作しています。
2. `「ci通したら、レビュー依頼出してもらえますか？」`って言うコミュニケーションをしておくだけで、スムーズになった。
3. `Dangerfileの管理`が格段に楽になった！！

# 今後の課題

1. 業務では、mergeしたら、buildを実行する運用をしているので、「Only Build pull requests」はoffにしているので、Pull Requestをcreateしたら、buildを実行されるようにしたい。今まで↓みたいに、1日1回深夜に実行したりしていたのですが、無駄にbuildが実行されてしまうので、どうにかしたい。

```yaml
scheduled-workflow:
    triggers:
      - schedule:
          cron: "0 18 * * *"
          filters:
            branches:
              ignore:
                - master
    jobs:
      - danger:
          context: danger
```

まさに、2日目の、@yasuhiroki さんの
[CircleCIのOnly build pull requestsをoffにしてもプルリクエストを作ったらJobを実行したい](https://qiita.com/yasuhiroki/items/bddb3f51bf3439652c0c)
が参考にやってみようかなと思う！
2. dangerのdocker imagesのrubyのバージョンをあげたい。（ruby2.3系は2018年12月にサポートが切れるので。。）
3. CircleCIのAdventcalendarで言う事かわかりませんが（笑）、TravisCIでも実行できるようにしたい。

# さいごに

「ここは違うだろ！」「ここはこうした方がいいよ！」
とかあれば、いつでも、コメントください。非常に喜びます。