RubyのDangerでiOSプロジェクトのプルリクエストを自動レビューする

# はじめに
最近参画したプロジェクトで、Dangerというツールを取り入れて、PRレビューを一部自動化していて素敵だなと思ったので、自分でも調べてみようと思いキャッチアップした内容を記事として書くことにしました。

# Dangerとは？

[README](https://github.com/danger/danger?tab=readme-ov-file#what-is-danger)の内容を引用させてもらいます。

>Danger runs during your CI process, and gives teams the chance to automate common code review chores.
>
>This provides another logical step in your process, through this Danger can help lint your rote tasks in daily code review.
>
>You can use Danger to codify your teams norms. Leaving humans to think about harder problems.

日本語訳

>Danger は CI プロセス中に実行され、チームに一般的なコードレビュー作業を自動化する機会を提供します。
>
>これにより、プロセスに別の論理的なステップが追加され、この Danger を通じて、毎日のコード レビューで定型タスクをリンティングできるようになります。
>
>Danger を使用すると、チームの規範を体系化できます。より難しい問題については人間に考えさせることができます。

タイトルにも少しありますが、コードレビューの一部を自動化してくれるツールのようです。
定型的なコードレビューを自動化することで、レビュワーは設計や仕様など本質的な部分に集中してレビューすることができるようになるそうです。素敵やん！

# 導入してみる

## 環境
- IDE: Xcode16.2
- Swiftバージョン: Swift5.9
- プロジェクト構成: マルチモジュール構成(SwiftPM)
- CI: GitHub Actions

## 手順
[こちらの記事](https://qiita.com/toya108/items/d27873c6d138ffcce72c)をめちゃくちゃ参考にしました！ありがとうございます！！

まず前提として、Dangerを導入したいリポジトリのルートディレクトリに移動しておいてください。

### 1. Dangerをローカルにインストール

Gemfileをリポジトリのルートディレクトリに作成します。
```ruby: Gemfile
source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'danger', '9.5.1' # 最新のバージョンを固定でインストールするようにしとく
```

Gemfileが作成できたら、以下コマンドで`danger`をインストールします。(bundlerを使ってインストールするので、なければ[こちら](https://qiita.com/toya108/items/d27873c6d138ffcce72c#1-bundler%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0)を参考にしてみてください)

```console
bundle install
```

以下コマンドでバージョンが確認できたらOKです。

```console
% danger --version
9.5.1
```

### 2. Dangerfileを生成する

Dangerfileというファイルに、なんの項目をどのようにレビューしてもらうのかなどを指定します。
ここではそのDangerfileを生成していきます。

といっても、生成自体は以下コマンドで一発でできます。

```console
danger init
```

途中で色々聞かれますが、Danger を CI で動かすための説明を丁寧にしてくれているだけなので、気になる人以外は飛ばして良さそうです。

Dangerfileが生成されていることが確認できたら、試しに以下のように修正してみましょう。

```ruby: Dangerfile
# Sometimes it's a README fix, or something like that - which isn't relevant for
# including in a project's CHANGELOG for example
declared_trivial = github.pr_title.include? "#trivial"

warn("PR大きすぎるよ！500行以下にしてもらえるとレビュワーが泣いて喜びます！😺") if git.lines_of_code > 500
warn("PRのタイトルが短すぎるよ！5行以上にしてね！🐶") if github.pr_title.length < 5
warn("PRの説明が短すぎるよ！レビュアーが見て分かる説明を書いてね！🦈") if github.pr_body.length < 100
warn("PRにassigneeが設定されてないよ！👹") unless github.pr_json["assignee"]
```

### 3. ローカルで作成したDangerfileが機能するのか試してみる

DangerをCIでぶっつけ本番で試すのは、CIで余計なリソースを使うことになりますし、できればローカルである程度動作確認できるよ嬉しいですよね。

[こちらの記事](https://zenn.dev/morayl/scraps/0ffda6e7fd059b)にローカルで PR のレビューできる方法があったので、これで確認していきます。

確認する内容としては、ローカルからレビューしたいPRをDangerでレビューしレビュー内容を出力するというものです。
実際にそのPRにコメントするわけではないので、割と気楽に実行できます。

方法は簡単で、以下コマンドを実行すればOKです。
```
export DANGER_GITHUB_API_TOKEN=<GITHUBのトークン>
bundle exec danger pr <GithubのプルリクエストページのURL>
```

GitHubトークンは[こちらの記事](https://qiita.com/RyutoYoda/items/c6908025c0aef4b35cdd)を参照してみてください。
当然ではありますが、作成するGitHubトークンは、レビュー対象のリポジトリを参照する権限が必要ですので、権限のあるユーザーのGitHubトークンを作成するようにしてください。

実行したら、以下のような結果になりました。

```
% bundle exec danger pr <GithubのプルリクエストページのURL>
To use retry middleware with Faraday v2.0+, install `faraday-retry` gem
Running your Dangerfile against this PR - <GithubのプルリクエストページのURL>
Turning on --verbose

Info:
・・・省略
Results:

Warnings:
- [ ] PRの説明が短すぎるよ！レビュアーが見て分かる説明を書いてね！🦈
- [ ] PRにassigneeが設定されてないよ！👹
```

今回は以下PRに対してdangerを実行したので、PRの説明が短すぎるというのと、assigneeが設定されていない部分で怒られました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/00218b5b-4a46-4cfd-97de-c3c8add4408b.png)

ローカル確認では、きちんと動いていそうです！

### 4. CIでDangerを動かしてみる

GitHub ActionsでCIを実装します。

github actions のワークフロー yamlの例です。
以下ファイルを`.github/workflows`に配置したらPull Requestを作った時にとりあえずCIが動くようになります。

```yaml: ci.yaml
# This workflow will build a Swift project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift

name: ci

on:
  pull_request:
    branches: "*"

jobs:
  build:
    # 実行環境はmacosに設定
    runs-on: macos-latest

    steps:
      # チェックアウト(リポジトリからソースコードを取得)
      - uses: actions/checkout@v4.2.2

      # Rudy製ライブラリのキャッシュ
      - name: Cache Gems
        uses: actions/cache@v4.1.1
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-gems-

      # Rudy製ライブラリのインストール
      - name: Install Bundled Gems
        run: |
          bundle config path vendor/bundle
          bundle install --jobs 4 --retry 3

      # dangerを実行
      - name: Analytics with danger
        run: |
          bundle exec danger
        env:
          # Dangerはプルリクエストを読み取ってコメントをするので、必要な権限を持ったGithubトークンを渡してあげる必要がある
          DANGER_GITHUB_API_TOKEN: ${{secrets.DANGER_GITHUB_TOKEN}}
```

ただこれだけでは、CIは動いてもdanger実行時("Analytics with danger"のステップ)に失敗してしまうでしょう。
というのも、以下で`DANGER_GITHUB_API_TOKEN`という環境変数にGitHubのシークレット情報である`DANGER_GITHUB_TOKEN`を`${{secrets.DANGER_GITHUB_TOKEN}}`で取得する処理を入れていますが、このシークレット情報はデフォルトで入っているものではなく、開発者が定義する必要があるもののためなんの設定もしなければ、取得処理に失敗してしまいます。
```yaml
        env:
          # Dangerはプルリクエストを読み取ってコメントをするので、必要な権限を持ったGithubトークンを渡してあげる必要がある
          DANGER_GITHUB_API_TOKEN: ${{secrets.DANGER_GITHUB_TOKEN}}
```

シークレット情報の設定の仕方は[こちらの記事](https://qiita.com/mkin/items/75a4928a1fafe5eacd17#%E3%82%B7%E3%83%BC%E3%82%AF%E3%83%AC%E3%83%83%E3%83%88%E6%83%85%E5%A0%B1%E3%81%AE%E8%A8%AD%E5%AE%9A)にわかりやすく解説されていたので、ご参照ください。

シークレット情報には`DANGER_GITHUB_TOKEN`という名前でGitHubトークンを設定するようにしてください。(ローカルで確認した時と同じですね)

これで、CIを動かすためにやることは一通り終わったので、PRを作ってみましょう！

実際にPRを作成してCIでdangerを動かした結果、以下のようにコメントしてくれました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/c1a3ddf4-6ade-4f1b-bff9-1449f08d77fa.png)

#### オプション
ちなみに、dangerを動かす時に設定するGitHubトークンが開発者自身のアカウントから発行した場合、dangerが生成したコメントはあたかもそのアカウントの人がしたように見えてしまうため、実務で使う際は変なことになってしまいます。(この人レビューに参加してないのに、なんかコメントしてる！みたいな、、)

こうなるのはちょっと嫌なので、Danger用のGitHubアカウントを作ってあげて、そのアカウントのGitHubトークンを生成してdangerに指定してやるのが良さそうです。
注意点として、Danger用のGitHubアカウントには、対象のリポジトリの参照権限を与える必要があります。

# 静的解析によるレビューも入れたい

これまでの手順で、PRの自動レビューができるようになりましたが、PR作成のお作法的なところしかまだ自動レビューできなさそうです。

親切なことにSwiftLintで静的解析して警告やエラーが出ているところもDangerでコメントをつけてくれることもできるようです。
いよいよレビュワーが泣いて喜びそうです。

そこで、DangerでSwiftLintによる静的解析の結果によるレビュー自動化にも取り組んでみます。

## 1. DangerでSwiftLintを使うためのプラグインをインストール

DangerでSwiftLintを使うためには[danger-swiftlint](https://github.com/ashfurrow/danger-ruby-swiftlint)というプラグインをインストールする必要があります。

そこでGemfileを以下のように修正します。
```diff_ruby: Gemfile
source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'danger', '9.5.1' # 最新のバージョンを固定でインストールするようにしとく
+ gem 'danger-swiftlint', '0.37.0' # 最新のバージョンに固定
```

修正後、以下コマンドでインストールします。
```console
bundle install
```

## 2. Dangerfileを修正

続いて、Dangerfileを以下のように修正します。

```diff_ruby: Dangerfile
# Sometimes it's a README fix, or something like that - which isn't relevant for
# including in a project's CHANGELOG for example
declared_trivial = github.pr_title.include? "#trivial"

warn("PR大きすぎるよ！500行以下にしてもらえるとレビュワーが泣いて喜びます！😺") if git.lines_of_code > 500
warn("PRのタイトルが短すぎるよ！5行以上にしてね！🐶") if github.pr_title.length < 5
warn("PRの説明が短すぎるよ！レビュアーが見て分かる説明を書いてね！🦈") if github.pr_body.length < 100
warn("PRにassigneeが設定されてないよ！👹") unless github.pr_json["assignee"]

+ # PRで出た差分以外の部分に関しては無視する設定
+ github.dismiss_out_of_range_messages
+ # SwiftLintの設定
+ swiftlint.lint_files inline_mode: true
```

## 3. ローカルで確認

先と同じ手順で、ローカルから対象のPRに向けて自動レビューの動作確認をしてみます。

```console
Results:

Warnings:
- [ ] PRの説明が短すぎるよ！レビュアーが見て分かる説明を書いてね！🦈
- [ ] PRにassigneeが設定されてないよ！👹
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L72: Variable name 'ajfselijflsjafsejlfjasijflsajefiaslefjlasiejfliasej' should be between 3 and 40 characters long
`identifier_name` `TrainingTagClient.swift:72`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L69: Line should be 120 characters or less; currently it has 124 characters
`line_length` `TrainingTagClient.swift:69`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L72: Line should be 120 characters or less; currently it has 123 characters
`line_length` `TrainingTagClient.swift:72`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L28: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:28`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L35: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:35`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L38: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:38`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L41: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:41`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L61: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:61`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L64: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:64`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L67: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:67`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L75: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:75`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L78: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:78`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L81: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:81`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L93: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:93`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L96: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:96`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L98: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:98`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L102: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:102`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L104: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:104`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L107: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:107`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L109: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:109`
- [ ] Macho/MachoFramework/Sources/MachoView/Dependencies/TrainingTagClient.swift#L111: Lines should not have trailing whitespace
`trailing_whitespace` `TrainingTagClient.swift:111`
```

このようにSwiftLintでしっかり指摘してくれています。

ちなみに、DangerによるSwiftLintの静的解析対象は、デフォルトではPRででた差分の箇所に限られるので(実際に使う時もその方が都合が良いケースが多いはず)、確認したいときは差分箇所をSwiftLintに怒られるように修正しましょう。

## 4. CIで確認してみる

特に追加で行うことはなく、上記修正をPushしてPRで確認してみましょう。
私の手元では以下のような感じで、問題箇所にインラインでコメントを入れてくれるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/410829ba-6ffc-4af5-a4f0-08925170a31b.png)

## はまりどころ

SwiftLintの設定をDangerfileに追加して、Dangerを実行したときに以下エラーが発生するようになった。

```console
bundler: failed to load command: danger (/Users/satoutaichi/.rbenv/versions/3.1.6/bin/danger)
/Users/satoutaichi/.rbenv/versions/3.1.6/lib/ruby/3.1.0/psych/visitors/to_ruby.rb:430:in `visit_Psych_Nodes_Alias': \e[31m (Danger::DSLError)
[!] Invalid `Dangerfile` file: Unknown alias: unit_test_configuration\e[0m
 #  from Dangerfile:16
 #  -------------------------------------------
 #  swiftlint.binary_path = 'Macho/MachoFramework/.build/artifacts/swiftlintplugins/SwiftLintBinary/SwiftLintBinary.artifactbundle/swiftlint-0.56.1-macos/bin/swiftlint'
 >  swiftlint.lint_files inline_mode: true
 #  -------------------------------------------
```

これは、Dangerfileに追加した以下設定よりも上に、`swiftlint.lint_files`関連以外の設定を記述してしまうと発生するっぽいです。

`swiftlint.lint_files inline_mode: true`

例えば以下のようなDangerfileです。

```ruby
# PRで出た差分以外の部分に関しては無視する設定
github.dismiss_out_of_range_messages
# SwiftLintの静的解析設定
swiftlint.config_file = 'hoge/hoge/.swiftlint.yaml'
swiftlint.lint_files inline_mode: true
```

# おわり
Dangerを導入してみて、かなり便利な一方導入もかなり楽な印象でした。
Dangerの導入に関して、今回はRubyのものを扱いましたが、[Danger Swift]()というSwiftでDangerfileを実装できる導入方法もあるようです。
今度はそちらをキャッチアップして導入難易度や手順の複雑さ、出来ること違いなど見つつ、
どういう判断でどちらが良いのか考えてみたいと思います。