inSmartBank

B/43を運営する株式会社スマートバンクのメンバーによるブログです

GitHub Appを使ってDependabotが作るpull requestを自動マージさせる

こんにちは。皆さんは自身がメンテナンスするソフトウェアが依存するパッケージの更新、いわゆるdependency updateをどのような形で行っていますか?

SmartBankが提供するサービスB/43の開発では主にGitHubのDependabot version updates機能を用いて定期的なdependency updateを行っています*1。これは簡単にいえばGitHub repositoryにYAMLファイルを置いておくだけで自動的かつ定期的にversion updateのpull requestを作ってくれる便利なやつです。

便利ではあるのですが、アプリケーション規模やチーム体制によっては日々作成されるpull requestをさばくのに苦労することがあります。本記事ではそのような運用課題を解決するために導入した、GitHub Appを使った自動マージについて解説します。

この記事でわかること

  • Dependabotが作るpull requestを自動マージさせる方法
  • GitHub Appの作成方法(自動マージを実行する用途に特化した内容です)
  • 自動マージに関するポリシー策定
  • 自動マージを実現するうえでの注意点やハマりどころ

この記事で書かないこと

本旨に関わる部分については適宜説明しますが詳細は割愛します。必要に応じて公式ドキュメントをご参照ください。

解決したかった課題

ソフトウェア開発において依存するライブラリを最新に保つのは重要だとチーム全員が理解しつつも、Dependabotが毎週作成するpull requestsとchangelogに目を通し、approveし、mergeするという手作業を長期にわたって継続するのは大変なものです。

特にアップデートが多い週だと60個超のpull requestsが積まれることもあり、繁忙期には後回しになったり、一括でapproveするシェルスクリプト*2をいじらしく書いたり、あるpull requestをmergeしたあとに起きるconflictをポチポチ解消したりしており...この作業はチームにとってトイルでした*3

長らくそのような運用を続けてきたけれども、振り返ってみればテストが失敗しない限りユーザーや開発者に影響を与えたことがないとわかり、この度はトイル削減のために自動マージに取り組むことにしました。

実現したいこと

端的に言うと開発者が行っていたapprove, merge作業の自動化です。

人間が確認している時と同様にテストやビルドなど「CIのチェックをPASSする場合のみDependabotの作成するpull requestを自動マージさせる」のが要件となります。加えて今回は以下の制約がありました。

  1. 自動マージ後にbase branchでテスト等のworkflowを実行する
    • base branchでテスト等のworfklowが実行されないとHEADが知らぬ間に壊れてしまう可能性がある
  2. Personal Access Tokenを使わない
    • 特定の個人に紐付けるのはリスクである
    • 人の代わりにマシンユーザーを用意しても権限管理・トークンの有効期限管理・費用等の問題が生じる

実現方法

先に結論を書くと、上記の実現したかったことは3ステップで実現できました

  1. GitHub Appを作成する
  2. GitHub AppのApp IDと秘密鍵をsecretsに追加する
  3. 自動マージさせたいGitHubのレポジトリに必要な設定を行う

GitHubがEnable auto-merge on a pull requestにて自動マージについて解説しており、これをベースに実現しています。しかしながら同ドキュメントの例ではsecrets.GITHUB_TOKENを使用するため「自動マージ後にbase branchで別のworkflowを実行する」が満たせません*4。代案としてPersonal Access Tokenを使うことも可能ですが、この案も今回の制約に抵触したため、GitHub Appを使ってトークンを発行する方法を選びました。

なお、このような制約がなく「Personal Access Tokenやsecrets.GITHUB_TOKENを使っても良い」という方は1と2をスキップすることができます。3のステップで紹介するworkflowで任意のトークンを使うだけでOKです。

以降では各ステップについて詳述します。

1. GitHub Appを作成する

GitHub Appを作成します。公式ドキュメントのCreating a GitHub Appを参照すると操作手順がわかりやすいです。作成時の最低限の入力内容を示します。

項目
GitHub App name 任意(e.g. smartbank-auto-merge-bot )
Homepage URL 任意(e.g. https://github.com/smartbank-inc
Webhook 不要なので Active のチェックを外す
Repository permissions Repository sectionの以下の3つにRead and write権限を与える
- Pull request (approve/merge操作で必要)
- Contents (merge操作で必要)
- Projects (PR titleの変更等で必要)
Where can this GitHub App be installed? Only on this account > Only allow this GitHub App to be installed on the %{あなたのorganiation名} account.

このとき、Organizationの管理者権限を持っている人が操作しないと、Appのインストール先としてOrganizationを選択できない点に注意してください。*5


作成が終わったら自動マージさせたいレポジトリへのアクセス権をGitHub Appに与えておきます。GitHub Appの設定ページでRepository access > Only select repositoriesを操作し、対象レポジトリを許可すればOKです。

2. GitHub AppのApp IDと秘密鍵をsecretsに追加する

作成したGitHub AppのSettingsページ内にApp IDの表記があるので控えておきます。また、同ページにて認証で用いる秘密鍵(.pemファイル)を作成し、この中身も控えておきます。

入手した2つをOrganization secretsとして登録します*6。名前はsecretとしてvalidであればなんでもOKです。

項目 名前の例
App ID AUTO_MERGE_BOT_APP_ID
秘密鍵 AUTO_MERGE_BOT_PRIVATE_KEY

このときにsecretsをDependabot secretsとして登録しなければならないことに注意してください。Actionsのsecretsとして登録してしまうと、Dependabotによるpull request作成でトリガーされるworkflowでsecretsを参照できないためです。

https://i.imgur.com/pr4ZpxP.png

登録したsecretsに対して、Only select repositoriesで自動マージさせたいrepositoryだけを許可します。

3. 自動マージさせたいGitHubのレポジトリに必要な設定を行う

ここからは自動マージさせたいレポジトリごとの設定になります。

Allow auto-mergeをONにする

まずは自動マージさせたいGitHubのレポジトリの設定でAllow auto-mergeをONにします。設定箇所は公式ドキュメントのスクリーンショットを参考にしてください。

branch protection ruleを設定する

次に、必要なCIチェックが通ったときのみマージさせたいのでbase branchのbranch protection ruleを設定します。

Require status checks to pass before mergingという設定があるのでこれを有効化し、必要なCIチェックが通るまでmerge不可とします。この設定は自動マージに限らずやっておいたほうがいいものですね。

workflowを追加する

あと一息です。最後に以下のworkflow設定を追加します。ファイル名は任意で、弊社では.github/workflows/dependabot-automation.ymlとしました。

name: Dependabot automation

on:
  pull_request:
    types:
      - opened

jobs:
  dependabot-automation:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'dependabot[bot]' }}
    timeout-minutes: 13
    steps:
      - name: Generate token
        id: generate_token
        uses: actions/create-github-app-token@v1
        with:
          app_id: ${{ secrets.AUTO_MERGE_BOT_APP_ID }}
          private_key: ${{ secrets.AUTO_MERGE_BOT_PRIVATE_KEY }}
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v1.3.6
        with:
          GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
      - name: Approve & enable auto-merge for Dependabot PR
        if: |
          steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
          steps.metadata.outputs.update-type == 'version-update:semver-minor'
        run: |
          gh pr review --approve "$PR_URL"
          gh pr edit "$PR_URL" -t "(auto merged) $PR_TITLE"
          gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          PR_TITLE: ${{ github.event.pull_request.title }}
          GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}

公式ドキュメントの例との主な差分は以下です。

  • timeoutを設定
  • pull requestタイトルに(auto merged)を記載
  • GitHub Appとactions/create-github-app-tokenを用いてトークンを発行し、後続のステップで参照
    • (2023-09-14追記: 記事公開時はtibdex/github-app-tokenを紹介していましたが、公開後に公式のActionがリリースされたため差し替えました)
  • secrets.GITHUB_TOKENを使用しないのでpermissions設定は不要

GitHub Appに関係ない箇所はマネーフォワード社のDependabot 運用を自動化したいから一部拝借しました。同記事は「CIがpassした場合のみマージする」をbranch protection ruleとauto mergeの組み合わせで実現する仕組みについて詳しく解説しており、大変参考になりました。


これにて自動マージが動くようになりました 🎉

https://i.imgur.com/b56Vusd.png

今後新たなレポジトリを追加する場合は以下の3ステップを行えばOKです。

  • GitHub Appのインストール先レポジトリに追加
  • Organization secretsを参照可能なレポジトリに追加
  • 「3. 自動マージさせたいGitHubのレポジトリに必要な設定を行う」をやる

ちなみに、3のうちworkflowの追加についてはOrganizationのreusable workflow等で一括管理する案もあったのですが、レポジトリ固有の設定を行うシーンが多そうと判断したため見送りました。これについては後述します。

自動マージのルールと言語ごとの運用

さて、ここからは弊社固有の話です。

上記の説明では触れませんでしたが、自動マージしてよい条件はレポジトリ毎に異なります。大前提として「CI statusがPASSすること」という条件はすべてのレポジトリにおいて共通しているのですが、その他にも細かいルールを決める必要がありました。

  • Semantic versioningにおいてmajor, minor, patchのどこまでを自動マージすべきか?
  • たとえpatchやそれ以下のversionであっても自動マージさせたくないgemやpackageはあるか?

これらのルールはレポジトリで扱う対象の言語や依存するpackage固有の問題、加えて各チームの考え方に依存します。弊社では議論や筆者のオーナーシップや独断等により以下のルールとしました。

言語 自動マージされるversion type その他条件
Ruby (Rails) minor, patch minor versionはdevelopment groupのみ
Go minor, patch 特になし
Node.js minor, patch レポジトリによってはvite, react-router等は除く

ルールに沿ったフィルタを行うにはdependabot/fetch-metadataのoutputsを利用します。言語によって利用できる設定が異なるのでREADMEのUsage instructionsをさらっと眺めておくと便利です。

なお、GitHub Actionsのログからdependabot/fetch-metadataで得られるmetadataの中身を見られるのが地味に便利で、ルール策定の過程で役立ちました。

https://i.imgur.com/32nfWTA.png

ここからは各言語ごとの設定と意思決定の背景について説明します。*7

Ruby (Rails) アプリケーションの場合

筆者の主観ですが、Ruby gemsはminor versionでもアグレッシブな変更を含むことがまれによくあります。そのため自動マージ対象は消極的に「patch version」または「minorかつdevelopment group」としました。

if: |
  steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
  (steps.metadata.outputs.update-type == 'version-update:semver-minor' && steps.metadata.outputs.dependency-type == 'direct:development')

弊社の場合は最も重要なコアアプリケーションの1つがRubyで書かれており、このアプリケーションの自動マージは安全側に倒したいというのも理由の1つです。

Go アプリケーションの場合

Goに詳しい同僚の@nilpoonaに聞いたところ、Goではminor versionではしっかり互換性を保つライブラリが多い印象とのことでした。そのため自動マージは積極的に「patch version」または「minor version」を対象としました。

if: |
  steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
  steps.metadata.outputs.update-type == 'version-update:semver-minor'

余談ですが、Goのpackage managerにはグルーピング機能(Gemfileで言うところのgroup: :developmentみたいなやつ)が無いためdependency-type == 'direct:development'のようなフィルタはできません。このフィルタが利用可能なpackage managerはbundler, composer, mix, maven, npm, pipのみとのことです。

Node.js アプリケーションの場合

過去の経験からminor versionでは一部packagesを除いてそこそこ互換性を保つライブラリが多い印象です。また、アップデートの数や頻度が最も多いのもnpm packagesです。そのため自動マージ対象は積極的に「patch version」または「minor version」、ただし「一部のpackageは除く」としました。以下は過去にminor versionのアップデートで非互換が含まれがちだったvitereact-routerを自動マージ対象から除いた例です。

if: |
  (steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
   steps.metadata.outputs.update-type == 'version-update:semver-minor') &&
   !contains(steps.dependabot-metadata.outputs.dependency-names, 'vite') &&
   !contains(steps.dependabot-metadata.outputs.dependency-names, 'react-router')

自動マージ設定を導入する際にこれらのルール策定にも悩みました。しかし、自動マージ直後に本番環境にデプロイされるわけではなく検証のタイミングがあるので、まずは積極的に挑戦してみることにしました。

自動マージ導入の結果

本記事の執筆時点で自動マージを有効化してから3週間ほど経過しており、Dependabotが作成したpull request 162件のうち102件が自動マージされ、約66%の自動化に成功しています。

また、自動マージによる障害やバグといったユーザー影響は今のところ発生していません

当初に思っていたより考えることや対応の手数が多くて対応に時間がかかりましたが運用が楽になりとても満足しています。

注意点 / ハマったこと / tips

最後に落ち穂拾いです。自動マージを実現するにあたりハマったことや気付いたことを記録しておきます。どなたかのお役に立てば幸いです。

secrets.GITHUB_TOKENによるマージでは他のworkflowがトリガーされない

(本記事の流れと異なるのですが)実は当初はsecrets.GITHUB_TOKENで自動マージにトライしていました。

base branchのHEADでCIが実行されていないことに気付き、この方式ではマズいと考え直し、GitHub Appを使うことにしました。

このタイミングで悩んでいたらGitHub Appの情報をtweetで教えてくれた@yuya_takeyamaさんに感謝です🙏

Actions secretsとDependabot secretsは別物

先述しましたがActions secretsに設定したsecretsは、Dependabotが作成したpull requestによりトリガーされるworkflowからは参照できません。secretsを参照できないとき、workflow実行時に以下のエラーが起きます。

Error: Error: Input required and not supplied: app_id

https://i.imgur.com/TxzBnFZ.png

後で気付いたのですが公式ドキュメントのトラブルシューティングの項に記載があります。ちゃんとドキュメントを読もう案件でした。

GitHub Appのpermissionsの変更が反映されない

GitHub Appのpermissions設定に不足があり以下のエラーに遭遇しました。発行したトークンでpull requestの操作ができないというものです。

GraphQL: Resource not accessible by integration (repository.pullRequest.projectCards.nodes)
Error: Process completed with exit code 1.

https://i.imgur.com/xLtXHSt.png

エラー発生後にGitHub Appのpermissions設定変更を行ったのですが解消せず。なんでだ〜と思っていたら、GitHub Appのpermissions変更はSave changesをクリックしただけでは反映されず、そのあとに変更をacceptしないといけないようでした。

💭 操作者が自分で変更リクエストを出して自分でacceptするという謎のフローだな...と思いました。

Code ownerのレビューを必須にしていると自動マージできない

branch protection rulesでCode ownerのレビューを必須にしている場合は自動マージできません。

筆者が知る限りではGitHub AppをCode ownerに加えることはできないため、branch protection rulesを緩めるなどCode ownerの設定変更で回避するしかないように思います。

dependabotのupdate checkを手動で起動したい

最後にtipsです。

Dependabotのupdate check ~ pull request作成は通常は定期実行でトリガーされますが、GitHub repositoryのInsights > Dependency graph > Dependabot > Last checked N days ago -> Check for updatesボタンをクリックすることで手動で起動できます。

https://i.imgur.com/HDgRVjm.png

workflowのテストをするためにDependabotに動いてほしい機会があり、調べていたら辿り着くことができました。

終わりに

オフトピックです。本記事の執筆直前にruby-jp Slackにてたまたま@hsbtさんが同じことをしているのを見かけてちょっとだけ情報交換させてもらいました。彼がブログで書かれていたことにめちゃめちゃ共感したので引用して締めたいと思います。

似たようなことを、それぞれが求めるちょっとだけ違う要件の下で人々が無限に作ってるの面白すぎる。

もう少し、何かがこう、なんか良い感じになるといいですね。

〜完〜


本記事はSoftware Engineerの@ohbaryeが執筆しましたが、各種設定やテストにあたってSREの@maaaatoに大いに協力してもらいました。この場を借りて今一度同僚への"感謝"を捧げます。

SmartBankでは自動化やトイル削減に関心のあるエンジニアを募集しています!

*1:一部レポジトリではrenovateを使っています

*2:こんなのです $ for pr in $(gh pr list --json 'number' -t '{{ range $i, $pr := . }}{{ $pr.number }} {{end}}' --label dependencies); do gh pr review -a $pr; done

*3:手作業、繰り返される、自動化が可能、戦術的、長期的な価値がない、サービスの成長に比例して増加する、といった特徴を持つ作業の意。 https://cloud.google.com/blog/ja/products/gcp/identifying-and-tracking-toil-using-sre-principles?hl=ja 参照

*4:再帰的なworkflowの実行を防ぐための仕様です

*5:もし個人アカウントに紐づけて作ってしまった場合でも移譲することができます。メドピア社のGitHub Appsの作成とOrgへの所有権の委譲手順を参考にAppの移譲を行うと良いでしょう

*6:レポジトリごとのsecretsとして登録してもよいですが一元管理できるほうがよいのでそうしています

*7:実際の設定そのままではありません

We create the new normal of easy budgeting, easy banking, and easy living.
In this blog, engineers, product managers, designers, business development, legal, CS, and other members will share their insights.