Dependabot Pull Request の auto approve と auto merge

[Dependabot](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/about-dependabot-version-updates) が作成した Pull Request を自動で approve & merge するためにやったことを紹介します。

## やりたいこと

Dependabot の対象ブランチは[ブランチ保護ルール](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches)が設定されており、CI が成功した上でレビューにより 1 件以上の approve がなければマージできないものとします。

実現したいのは次のような自動化です。

- **パッチバージョンだけの更新 Pull Request なら自動で approve したい**
  - パッチバージョンアップデートによって問題が起こることはほとんどないため、人力レビューを省きたい
  - 問題が起こるとしたらライブラリにバグがある場合だが、そういったケースは人力レビューでも見つけられないことが多いので許容する
      - CI でしっかりテストを実行することによって動作を保障する
- **マイナーバージョン以上の更新は手動でレビューする**
  - 一部のライブラリはマイナーバージョンアップデートでも非互換の変更を行うことがあるため、人力レビューを必須とする
  - マイナーバージョンアップデートでは機能追加が行われることが多いので、あえて人力レビューを行うことで新機能を確認する機会にすることができる
- **自動か手動かを問わず、approve された Pull Request は自動でマージしたい**
  - パッチバージョンアップデートの場合、approve から merge をすべて自動化して人の手が一切必要ない状態にしたい
  - マイナーバージョン以上のアップデートの場合は手動 approve した後に手動 merge (必要に応じて手動 rebase) するわずかな手間を省きたい

## 公式ドキュメントの例を参考にする (失敗)

(こちらは失敗例なので結論だけ見たい人は[成功例](#dependabot-merge-でマージさせる-成功)まで飛んでください)

### 公式ドキュメントの自動化例

Dependabot の公式ドキュメントに GitHub Actions による自動化例が掲載されています。

https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/automating-dependabot-with-github-actions

#### Approve a pull request

`Approve a pull request` は auto approve の例です。

https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/automating-dependabot-with-github-actions#approve-a-pull-request

(上記 URL から引用)

```yml
name: Dependabot auto-approve
on: pull_request_target

permissions:
  pull-requests: write

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'dependabot[bot]' }}
    steps:
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v1.1.1
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
      - name: Approve a PR
        run: gh pr review --approve "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
```

この例では Dependabot の Pull Request をすべて auto approve しています。

[dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) Action を使用することで更新内容に関するデータを取得していますが実際には使用していません。

また、approve は [GitHub CLI](https://cli.github.com/) で行っています。
GitHub CLI は GitHub ホストランナーであればプレインストールされています。

#### Enable auto-merge on a pull request

`Enable auto-merge on a pull request` は auto merge の例です。

https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request

(上記 URL から引用)

```yml
name: Dependabot auto-merge
on: pull_request_target

permissions:
  pull-requests: write
  contents: write

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'dependabot[bot]' }}
    steps:
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v1.1.1
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
      - name: Enable auto-merge for Dependabot PRs
        if: ${{contains(steps.metadata.outputs.dependency-names, 'my-dependency') && steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
        run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
```

こちらの例では更新対象のライブラリが `my-dependency` という名前で、かつパッチバージョンアップデートの場合にのみ auto merge を行っています。

[dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) Action によって取得したメタデータを `if` で参照することで、auto merge の必要性を判断しています。

また、auto merge は GitHub CLI によって [Pull Request の auto-merge 設定](https://docs.github.com/en/github/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/automatically-merging-a-pull-request) を有効することによって実現しています。
auto-merge 設定が有効な Pull Request はブランチ保護ルールの条件 (CI の成功やレビューで approve されること) が満たされると自動的にマージされます。

### 公式ドキュメントの例をアレンジする

公式ドキュメントの例だと微妙に「やりたいこと」を満たせていないので次のようにアレンジしました。

#### Depenbdabot auto-approve

```yml:.github/workflows/dependabot-auto-approve.yml
on:
  pull_request_target:
    types:
      - opened

permissions:
  pull-requests: write

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'dependabot[bot]' }}
    steps:
      - name: Dependabot metadata
        id: dependabot-metadata
        uses: dependabot/fetch-metadata@v1.1.1
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
      - name: Approve a PR
        if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-patch' }}
        run: gh pr review --approve "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

[dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) Action によって取得したメタデータを `if` で参照することで、パッチバージョンアップデートの場合にのみ auto approve を行うようにしました。

また、workflow のトリガーを「Pull Request の作成時」のみに変更しています。
これは公式の設定例のままだと rebase 時などにも approve が発生してうるさいためです。

#### Dependabot auto-merge

```yml:.github/workflows/dependabot-auto-merge.yml
name: Dependabot auto-merge
on: pull_request_target

permissions:
  pull-requests: write
  contents: write

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'dependabot[bot]' }}
    steps:
      - name: Enable auto-merge for Dependabot PRs
        run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

Dependabot の Pull Request はすべて auto-merge 設定を有効にするようにしました。
マイナーバージョン以上のアップデートの場合は手動で approve しない限りマージはブロックされ、approve 後に (CI が通っていれば) 自動でマージされるはずです。

### 問題点

実際に上記のような workflow を設定してみたところ、いくつかの問題があることが分かりました。

#### auto-merge の設定に失敗する

実際に動作させたところ、`Dependabot auto-merge` workflow の `Enable auto-merge for Dependabot PRs` step が次のようなエラーで失敗してしまいました。

```
Pull request is not in the correct state to enable auto-merge
```

次の issue comment によると保護ブランチに対する Pull Request の場合、作成された直後に auto-merge を有効にしようとすると競合状態となり上記のようなエラーが出ることがあるようです。
(close されている issue ですが 2021 年 10 月現在も問題は継続しています)

https://github.com/cli/cli/issues/3514#issuecomment-893344039

#### rebase は手動で行う必要がある

Dependabot の Pull Request が作成された後、他の変更が対象ブランチに push されると次のような理由で rebase が必要になることがあります。

- マニフェストファイルやロックファイルが変更され、conflict が発生した
- ブランチ保護ルールで [Require branches to be up to date before merging](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches#require-status-checks-before-merging) (Pull Request が base ブランチに対して最新の状態でないとマージできない設定) が有効になっている

このような場合、`@dependabot rebase` コマンドを実行 (`@dependabot rebase` とコメント) して Dependabot に rebase を実行させるなどの対応が必要となります(条件は不明ですが、変更をトリガーに Dependabot が勝手に rebase してくれることもあります)。
手動でコマンドを叩く必要があるのではせっかく rebase した意味もあまりなくなってしまいます。

## `@dependabot merge` でマージさせる (成功)

前述の問題点を解決するために、auto merge の方法を変えてみることにしました。

Dependabot が作成した Pull Request の場合、`@dependabot merge` コマンドを実行 (`@dependabot merge` とコメント) すると Dependabot が必要に応じて rebase を実行した上で merge まで行ってくれます。
これを利用し、Dependabot が作成した Pull Request が approve されたことをトリガーに GitHub Actions で `@dependabot merge` とコメントし自動マージさせることにします。

この方法であれば Pull Request の auto-merge を有効にしないので `Pull request is not in the correct state to enable auto-merge` エラーを回避できます。
また、rebase を実行した上でマージしてくれるため手動で `@dependabot rebase` を叩く必要がなくなるはずです。

ちなみに同じ手法を採用した GitHub Actions として [ahmadnassri/action-dependabot-auto-merge](https://github.com/ahmadnassri/action-dependabot-auto-merge) があります。
サードパーティの Action を使うことに抵抗がないのであればこちらを採用するのも良いかと思います([サードパーティの GitHub Actions 使用にはセキュリティリスクがあります](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions))。

### workflow の設定例

前提条件として対象リポジトリに書き込み権限のあるユーザーの repo スコープが付与された [Personal Access Token (PAT)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) が [Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) に保存されている必要があります。

これは `@dependabot merge` の実行に必要な権限が [permissions](https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#permissions) 設定では与えられないためです(多分…与えられる方法を知っている方がいらっしゃいましたら教えてください)。
強めの権限を持つ PAT を扱うことになるため、その分のセキュリティリスクがあることは認識しておく必要があります。
参考にする場合は自己責任でお願いします。

以下の例では PAT を `DEPENDABOT_AUTOMATION_TOKEN` という名前で Secrets に保存しています。

#### Depenbdabot auto-approve (変更版)

```yml:.github/workflows/dependabot-auto-approve.yml
name: Dependabot auto-approve
on:
  pull_request_target:
    types:
      - opened

permissions:
  pull-requests: read

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'dependabot[bot]' }}
    steps:
      - name: Dependabot metadata
        id: dependabot-metadata
        uses: dependabot/fetch-metadata@v1.1.1
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
      - name: Approve a PR
        if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-patch' }}
        run: gh pr review --approve "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GITHUB_TOKEN: ${{ secrets.DEPENDABOT_AUTOMATION_TOKEN }}
```

`GITHUB_TOKEN` ではなく Secrets に保存した PAT を使うようにしています。
これは「`GITHUB_TOKEN` を使用した操作により発生したイベントから新しい workflow を実行させることはできない」という制限を回避するためです。

https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#triggering-new-workflows-using-a-personal-access-token

これは workflow の無限ループを防止するための制限ですが、PAT を使用した場合はこの制限を受けません。
ただし無限ループの危険性が伴うためこの方法を使う場合は慎重に workflow を設定する必要があります。

この変更により、auto approve をトリガーに auto merge を実行することができるようになります。

#### Dependabot auto-merge (変更版)

```yml:.github/workflows/dependabot-auto-merge.yml
name: Dependabot auto-merge
on:
  pull_request_review:
    types:
      - submitted

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.event.review.state == 'approved' }}
    steps:
      - name: Order Dependabot to merge
        run: gh pr comment --body "@dependabot merge" "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GITHUB_TOKEN: ${{ secrets.DEPENDABOT_AUTOMATION_TOKEN }}
```

トリガーを `pull_request_review` の `submitted` に変更し、`github.event.review.state` が `approved` であることを確認することで approve された場合にのみ実行するようにしています。
approve をトリガーとしたのはマージ条件を満たしていない状態で `@dependabot merge` を実行すると Dependabot がマージできない旨をコメントしてきてうるさいためです。
気にならないのであれば `pull_request_target` のままでも問題ありません(この場合、auto approve 側で PAT を使う必要がなくなります)。

トリガーを変更した影響で、`github.actor` はレビューを行ったアカウントになってしまっているので Dependabot が作成した Pull Request であることの判定には `github.event.pull_request.user.login` を使用しています。

`@dependabot merge` の実行は GitHub CLI で行っています。ここでは PAT を使用して認証する必要があります。

### 問題点

こちらの方法にもいくつか問題点が存在します。

#### Personal Access Token が必要となる

前述の通りですが、強めの権限を持つ PAT が必要なのでその分のセキュリティリスクがあります。
なるべく最小限の権限を持った専用の GitHub アカウントを作成して、トークンに期限を設けるなどの工夫をした方が良いかもしれません。

上記の例を参考にする場合は自己責任でお願いします。

#### approve をトリガーにすると Dependabot のメタデータが取得できない

[dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) Action は少なくとも v1.1.1 の時点では `pull_request_target` イベントをトリガーとして実行されることを前提に作られているため、approve をトリガーにすると使用できません。
そのため依存ライブラリの名前や更新の種別 (major, minor, patch) などによって自動マージするかどうかを制御することができなくなります。
メタデータが必要な場合は `pull_request_target` をトリガーとするようにしてください。

#### やっぱり手動での rebase が必要なこともある

`@dependabot merge` も万能ではなく、マージしようとしたタイミングで他の Pull Request がマージされてしまったりするとタイミングを見失うのか処理が進まなくなることがあります。
そうなってしまった場合は残念ながら `@dependabot rebase` を手打ちしてあげる必要があります。