Github DependabotでライブラリアップデートPRを自動作成した際の動作確認をなるべく楽にした話

# そもそもDependabotとは

[Github](https://github.com/)に[Dependabot](https://docs.github.com/ja/github/administering-a-repository/keeping-your-dependencies-updated-automatically)という機能があるのはご存知でしょうか？
リポジトリ内で使用している依存関係（ライブラリ）で古いものがあれば更新をかけたPullRequestを自動発行してくれる機能です。
パブリックなリポジトリで以下のようなPRを見たことがある方も多いと思います。

![スクリーンショット 2020-09-09 20.12.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129872/113c485a-99c5-4a5a-bc8a-01897dad8d59.png)

もともとは独立したサービスでしたが、Githubに買収されてネイティブ機能として取り込まれたので導入がしやすくなりました。
`.github/dependabot.yml`配下に設定ファイルを配置するだけでライブラリアップデートPR自動作成を有効に出来ます。

今回Dependabot自体の解説は省きますが私の携わるプロジェクトでは以下のような設定ファイルで月曜日の9時にチェックがかかるように運用を始めています。

```yml
version: 2
updates:
  - package-ecosystem: "gradle"
    directory: "/"
    target-branch: "develop"
    schedule:
      interval: "weekly"
      day: "monday"
      time: "09:00"
      timezone: "Asia/Tokyo"
    reviewers:
      - "ignis-ltd/with-android"
```

まずはGithub Dependabotについて詳しく知りたいという方は以下の公式ドキュメントや記事が参考になると思います

- [依存関係を自動的に更新する - GitHub Docs](https://docs.github.com/ja/github/administering-a-repository/keeping-your-dependencies-updated-automatically)
- [GitHubのdependabotの挙動をカスタマイズする](https://blanktar.jp/blog/2020/08/customize-github-dependabot)

※現状「Dependabot」でググると買収される前の独立したサービスだった時代のマーケットプレイス版の記事がたくさん出てきますが、そちらはいずれ廃止されてネイティブ機能に統合される可能性が高いのでご注意下さい

# Dependabotの運用課題

Dependabotは1ライブラリのアップデートにつき1つのPRを発行されます。
一部のライブラリアップデートが問題を起こす可能性もあるので問題を切り分けるためにも個別になっている事自体は正しい形だと思っているのですが、いくつもアップデートがある場合やや課題が残ります。

![スクリーンショット 2020-09-09 20.12.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129872/431de7ae-af32-5f16-ee96-5d4b8e6ec5f2.png)

サーバーサイドでも似たような課題は発生しうると思いますが、私の扱うプロジェクトではAndroidアプリなので上記のように5つのPRが一度に発行された場合5回ブランチをcheckoutしてビルドしてインストールして動作確認をする必要がありました。

もちろんCI上でテストを組んで自動化すればリスクを減らすことも出来ますが、やはりアプリの場合はUIの崩れ等が心配なのでマージ前に目視での動作確認は1度はしておきたいところです。
あるいはCIでブランチごとにデプロイまで行えばチェックアウトまではしなくても良いですが、それでも5回ダウンロードしてインストールして起動するだけでもちょっと大変ですよね。

# 解決策

そこで考えたアプローチとして、**dependabotが発行したそれぞれのPRのブランチを一つのブランチへマージしてCIからビルドを行い、各PRにコメントとしてバイナリへのリンクを残す**という手法を検討しました。
以下で実際のやり方をご紹介致します。

### Dependabotが発行したブランチを統合する

まずアップデートがかかったGitブランチをすべて抽出して統合するRubyスクリプトを組んでみます。

1. **`dependabot/`の文字列から始まるブランチ一覧を取得する**
1. **現在のブランチ（develop）からアップデートcommitのパッチファイルを作成する**
1. **2.で取得したパッチファイルをすべて適用する**

（Ruby力は底辺なのでそのあたりは見逃してください :pray: ）

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

gem 'git'
```

```ruby:merge_dependabot_branchs.rb
require "git"

dependabotBranchs = []
git_client = Git.open(Dir.pwd)
git_client.fetch
git_client.branches.each do |branch|
  if branch.name.start_with?('dependabot/')
    dependabotBranchs.append(branch)
  end
end

return if dependabotBranchs.size <= 0

dependabotBranchs.each do |dependabotBranch|
  system("git format-patch -1 #{dependabotBranch.gcommit} --unified=0")
end
system("git apply 0001-*.patch --unidiff-zero")
```

#### ちょっとした解説

ここで肝となるのがパッチファイルの作成と適用方法です。
`git format-patch`でパッチファイルを生成していますが、**通常では前後３行の変更もDiffに含まれています**。
つまりアップデートがかかったライブラリの行が隣り合っているときなど、前後３行以内に変更がすでにある場合コンフリクトとして判断されてしまい自動での統合がされません。
Dependabotによるバージョンアップの変更だけを抽出すればコンフリクトは起こり得ないはずなので、近い行で変更があってもコンフリクトとみなさず統合してほしいものです。
そこで`--unified=0`パラメータを付与することで前後の行をDiffに含めないように調整しています。（[参考](https://git-scm.com/docs/git-format-patch#Documentation/git-format-patch.txt---unifiedltngt)）

また、`git apply`でパッチファイルを適用する際にも小技を使っていて、**通常では前後の行が含まれていない（`--unified=0`を指定した）パッチファイルでは適用が失敗してしまう**ので`--unidiff-zero`パラメータを付与して前後の行を無視して適用されるようにしています。（[参考](https://git-scm.com/docs/git-apply#Documentation/git-apply.txt---unidiff-zero)）

この辺の処理は当初使っていた[ruby-git](https://github.com/ruby-git/ruby-git)だと機能不足だったのでsystemコールに頼っています。

ここまででライブラリアップデートが統合されたブランチが出来上がっているはずなので、この状態でビルドすれば統合されたバイナリを生成できます。

### Dependabotが発行したPRに統合したバイナリへのリンクを貼る

CI上でバイナリを生成してリンクURLも発行したと仮定して、もともとのDependabotが生成したPRにコメントを残すRubyスクリプトを組んでみます。
今度はGitではなく[Github API](https://docs.github.com/en/rest)を使って抽出します。

1. **現在開かれているPullRequestで`dependabot/`の文字列から始まるブランチを抽出する**
2. **抽出したPullRequestに対してバイナリへのリンクと統合したPullRequest一覧をコメントとして貼る**

（Ruby力底辺コード）

```ruby:comment_dependabot_prs.ruby
require 'net/http'
require 'uri'
require 'json'

uri = URI.parse("https://api.github.com/repos/ignis-ltd/with_android/pulls")
request = Net::HTTP::Get.new(uri)
request["Accept"] = "application/vnd.github.v3+json"
request["Authorization"] = "token #{ENV['GITHUB_API_TOKEN']}"

req_options = {
  use_ssl: uri.scheme == "https",
}

response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
  http.request(request)
end

dependabotPullRequests = []
responseBodyJson = JSON.parse(response.body, symbolize_names: true)
responseBodyJson.each do |pull|
  if pull[:head][:ref].start_with?('dependabot/')
    dependabotPullRequests.append(pull)
  end
end

dependabotPullRequests.each do |pull|
  uri = URI.parse("https://api.github.com/repos/ignis-ltd/with_android/issues/#{pull[:number]}/comments")
  request = Net::HTTP::Post.new(uri)
  request.body = JSON.dump({
    "body" => "Deployed a binary that merged the following branches\n" + dependabotPullRequests.map { |pull| pull[:html_url] }.join(" ") + "\n\n#{ENV['INSTALL_PAGE_URL']}"
  })
  request["Accept"] = "application/vnd.github.v3+json"
  request["Authorization"] = "token #{ENV['GITHUB_API_TOKEN']}"

  req_options = {
    use_ssl: uri.scheme == "https",
  }

  response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
    http.request(request)
  end
end
```

#### ちょっとした解説

基本的には[Github API](https://docs.github.com/en/rest)を使って愚直に実装しているだけです。

上記のRubyスクリプトの実行には以下の環境変数とが必要となります。Github API用のURLも適宜変更して下さい
Githubのトークン情報は直接含めず、CIのシークレット環境変数などを使って下さい。

- **GITHUB_API_TOKEN** ... GithubへのコメントするためのTokenを指定して下さい
    - [個人アクセストークンを使用する - GitHub Docs](https://docs.github.com/ja/github/authenticating-to-github/creating-a-personal-access-token)を参照して取得してください
- **INSTALL_PAGE_URL** ... 生成したバイナリへのリンクURLを指定して下さい
    - 今回はCIでのビルドやデプロイまでは言及しないので省略しますが、アプリのCIは個人的には[Bitrise](https://www.bitrise.io/)推しです。

ここまで組むことができればDependabotが発行したPullRequestに以下のようなコメントを残すことが出来ます。
どのブランチを統合したのかと統合されたバイナリをどのPullRequestからも参照することが出来るようになります。

![スクリーンショット 2020-09-09 20.12.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129872/c5d9a4ee-26be-5609-e1a2-7ed95fa03dc8.png)

### Dependabotの実行後にCIでスクリプトとビルドを定時実行させる

私のプロジェクトではDependabotの実行を月曜9:00に行っているため、このスクリプトを含んだCIの実行は月曜10:00に行うように調整しています。
以下はBitriseでの設定例です。このあたりはお好みで調整して下さい。

![スクリーンショット 2020-09-10 2.43.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129872/2d63d1f0-5b06-b8e2-594c-603890f7a33a.png)

### 補足というか注意点

最初のDependabotのブランチを統合するスクリプトはGitのブランチ情報を基準にしてますが、後のコメントする際のスクリプトはPullRequest基準で検索をかけるので、（基本的にはないと思いますが）PullRequestが発行されていないDependabotブランチが存在する場合や、ビルド中にブランチやPRに変更を加えた場合はコメントの内容とバイナリの実態にズレが生じます。

また、`dependabot/`という文字列でブランチ名を前方一致検索しているので、これに該当するブランチを手動で作った際に誤作動を起こすのでもう少し厳密な判定ロジックを設けるべきかもしれないです（横着感）

# おわり

結構力技でしたが、ここまでやることで毎週の動作確認が1度で済ませられるのでコスト削減につながるかと思います。
小さなプログラムではそこまでライブラリが多くないと思うのでここまでする必要性も感じないかもしれませんが、10万行を超えてくる規模だと導入されているライブラリの数も膨大になり動作確認コストも馬鹿にできないので、このような対策を講じることでDependabotを最大限有効活用することが出来るようになりました。

また、今回DependabotをCI上で統合するお話ですが、スクリプトを書いて気合で統合してなんとかするという部分の話だけでCI側の話がほとんど出来ていないのでどこかでお話できたらと思います。