🐧

GitHub Actions による Renovate の安全自動マージ

2023/05/14に公開

本記事は Renovate の Pull Request を安全に自動マージするためのガイドです。

以下のドキュメントが元になっています。

https://suzuki-shunsuke.github.io/guide-github-action-renovate/

この記事は一度書いたら後はメンテしませんが元のドキュメントはメンテしていくので、元のドキュメントも参照してください。

Renovate の導入は非常に簡単ですが、単に GitHub App をインストールしただけではその真価は発揮されません。
この記事では安全かつ自動で Renovate の Pull Request をマージし dependencies を常に最新に保つためのテクニックを紹介します。

この記事では CI に GitHub Actions を使うことを前提とします。また、 Renovate とは直接関係ない内容も含みます。

1. 概要

ある dependency を update する場合、その update に関連した test が CI で実行され、 update の安全性が担保されるようにします。
pull_request 以外で実行される workflow に関しても dry run などで可能な限りテストします(コストなどの兼ね合いで難しい場合もあるでしょう)。
これは自動マージのためにも必須です。 CI が通っていればマージしてよし、という状況を目指します。 Pull Request は CI が pass したら即座にマージされるようにすることで、 Pull Request が溜まらないようにし、人間が一々手でマージしなくても良いようにします。

ただし一部の update については安全に自動マージするのが難しい場合もあるでしょうし、自動マージに対して拒否感を持つ人もいるでしょう。そういう場合は一部の update を自動マージから除外したり、自動マージの対象を段階的に広げていくようにします。

job の数が増えると毎回全ての job を実行するのは非効率なので、 job に関連するファイルが変更された場合のみ job を実行するようにします。ただしファイルが漏れると CI が壊れているのに気づけない場合もあるので、対象のファイルは若干広めに設定します。

チーム開発では要求されるセキュリティレベルに応じて Renovate の Pull Request を保護します。悪意のある人が Renovate の Pull Request を悪用し、レビューを回避してマージできてしまわないようにします。

2. サンプル

以下の workflow は参考になるでしょう。

https://github.com/aquaproj/aqua/blob/main/.github/workflows/test.yaml

3. default branch に branch protection rule を設定する

以下の branch protection rule を設定します (status-check については後述)。

  • Require a pull request before merging
  • Require status checks to pass before merging
  • Status checks that are required.: status-check

4. pull_request 用の GitHub Actions Workflow を 1 つにまとめる

一部の job は特定のファイルが変更されたときのみ実行したいでしょう。
その場合その job 用の workflow を作成し GitHub Actions の path filter を設定するのが一般的でしょう。
しかし、その場合その job を branch protection の status checks に追加してしまうと、その workflow が path filter で skip された場合に status checks が pass しなくなり pull request をマージできなくなるという問題があります。その job を status checks から除外すると今度はその job が失敗しているのに Pull Request がマージできてしまいます。

この問題を回避するために、 pull_request 用の workflow を一つにまとめ、 workflow の path filter を設定するのをやめ必ず workflow が実行されるようにします。そして job の if を指定し、特定のファイルが変更されたときのみ job が実行されるようにします。
status checks の job が skip されても Pull Request はマージできるので問題ありません。
dorny/paths-filter のような action を使うと簡単に job 単位で filter を設定できます。

特定の job は push event など pull_request event 以外の event でも実行したい場合もあるでしょう。その場合 workflow 自体は pull_request とそれ以外で分け、 Reusable Workflow を使い job を共通化するのが良いでしょう。

actionlint は「 pull_request 用の workflow を一つにまとめる」というルールの例外になります。
というのも actionlint を実行する workflow が壊れると actionlint が実行されなくて困るので、 actionlint だけは分離して壊れないようにする必要があるからです。

e.g. https://github.com/aquaproj/aqua/blob/main/.github/workflows/actionlint.yaml

5. status check 用の GitHub Actions job を用意する

branch protection rule で status check の job を一つ一つ指定すると job を追加・リネーム・削除する際に少々困ったことになります。
例えば status checks に含まれる A という job を B にリネームする Pull Request を作成した場合、そのままではその Pull Request はマージできません。まず status checks から A を除外し、 Pull Request をマージし、 B を status checks に追加する必要があります。
この間一時的に A が status checks から除外されるので A が失敗しても Pull Request がマージできる状態になってしまいますし、 B を追加するまでの間 B が失敗しても Pull Request がマージできる状態になってしまいます。

この問題を解決するために、専用の job status-check を用意し、 status-check だけを status checks に追加し、 status-check の needs に必要な job を追加していきます。こうすると branch protection rule を変更する必要がなくなります。
branch protection rule を変更する必要がないので、 branch protection rule を変更するための権限も必要なくなりますし、簡単にコード管理できる(branch protection rule はコード管理されていないという現場も多いでしょう)、 workflow の変更と同じ Pull Request でレビューできるというメリットもあります。

status-check job の中身は次のようになります。

  status-check:
    runs-on: ubuntu-latest
    needs:
      - update-aqua-checksums
      - test
      - build
      - renovate-config-validator
      - ghalint
    permissions: {}
    if: failure()
    steps:
      - run: exit 1

6. Renovate の Pull Request は可能な限り自動マージする

自動マージしたいものは Renovate の automergetrue にし、逆に自動マージしたくないものは false にします。

7. 危険な update は自動マージの対象外にする

major update は自動マージの対象外にするのが無難でしょう。 v0 の minor update も対象外にするといったこともありかもしれません。

8. GitHub の auto-merge を使う

Renovate の automerge によるマージはマージされるまでに時間がかかります。
Renovate の platformAutomerge は Pull Request が open されたときのみ auto-merge が enable になるので、一度 disable になると自動でマージされないという問題があります。

そこで GitHub Actions で auto-merge を enable にするのが良いでしょう。
merge commit で CI を実行したい場合、 ${{github.token}} の代わりに GitHub App の token を使います。

  enable-auto-merge:
    # Enable automerge to merge pull requests from Renovate automatically.
    runs-on: ubuntu-latest
    permissions: {}
    if: |
      github.event.pull_request.user.login == 'renovate[bot]' && contains(github.event.pull_request.body, ' **Automerge**: Enabled.')

    steps:
      - name: Generate token
        id: generate_token
        uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
        with:
          app_id: ${{secrets.gh_app_id}}
          private_key: ${{secrets.gh_app_private_key}}
      - run: gh -R "$GITHUB_REPOSITORY" pr merge --squash --auto --delete-branch "$PR_NUMBER"
        env:
          GITHUB_TOKEN: ${{steps.generate_token.outputs.token}} # Use GitHub App to trigger GitHub Actions Workflow by merge commit.
          PR_NUMBER: ${{github.event.pull_request.number}}

以下の if で Renovate の automerge が有効なときのみ実行するようにしています。

    if: |
      github.event.pull_request.user.login == 'renovate[bot]' && contains(github.event.pull_request.body, ' **Automerge**: Enabled.')

9. Reusable Workflow を使い job 毎にファイルを分割する

pull_request 用の workflow file が変更された場合、全ての job を実行する必要があります。どの job のコードが変更されたかわからないからです。ただ特定の job を変更しただけなのに関係ない全ての job を実行しなければならないのは非効率でしょう。
そこで Reusable Workflow を使い job 毎にファイルを分割すると不要な job 実行を減らすことができます。
job A の Reusable Workflow が変更されたとしても A と関係ない job を実行する必要がなくなります。

e.g.

  ghalint:
    needs: path-filter
    if: needs.path-filter.outputs.ghalint == 'true'
    uses: ./.github/workflows/wc-ghalint.yaml # ghalint を実行する job を分離
    permissions: {}

これは Reusable Workflow の中身が外部の Reusable Workflow を実行しているだけだったとしても意味があります。外部の Reusable Workflow が update された場合にその job と関係ない job を実行する必要がなくなるからです。

team 開発のために

1. (必要であれば) 自動マージの対象を段階的に広げていく

Renovate の Pull Request の自動マージを広めていこうとした際、自動マージに反対する人もいるかもしれません(いなければ読み飛ばしてください)。
人間によるレビューは必須である、 Release Note も見ずに自動マージはけしからん、といった考えもあるでしょう。
そういった場合、本当にリスクが少ないものから徐々に自動マージの対象にしていき、自動マージの有効性について理解してもらいつつ対象を広げていくのが良いでしょう。例えば application で直接使われている dependency よりは CI で使われている action, 例えば actions/checkout などのほうがリスクは小さく、説得も容易でしょう。 Renovate は manager, datasource, version, filepath などに応じて柔軟に automerge を on/off できます。一部の dependeny だけでも自動マージできれば人間の負担は減りますし、自動マージが正しく安全に動いていることが検証できるので対象を広げやすいしょう。

2. CodeOwner が設定されている場合の自動 approve

チーム開発では CodeOwner の review が必須になっているケースもあるでしょう。
その場合 GitHub App は CodeOwner になれないので、 Renovate の Pull Request を自動マージするには Personal Access Token を用いて approve する必要があります。専用の bot account (ただの GitHub Account) を作成しそのユーザーの access token を使うのが良いでしょう。
先日 fine-grained Personal Access Tokens で GraphQL API が実行できるようになったため、 fine-grained Personal Access Token を使うのが望ましいでしょう。
そのユーザーの access token が適切に管理されておらず色々なところで流用されてたり色々な人が access token を利用可能になっていると悪意をもって approve できてしまうので注意が必要です(たとえ新たに生成した token を適切に管理したとしても、既存の legacy token の扱いが不適切だとあまり意味がありません)。

Renovate の Pull Request のみで使う secrets に関しては GitHub Environment Secret を使って管理するのが良いでしょう。

3. (必要であれば) Renovate の branch に branch protection rule を設定し、人間が push できないようにする

team がより高い水準のセキュリティを要求する場合、 Renovate の branch に branch protection rule を設定し、人間が push できないようにする必要があるでしょう。
そうでなければ悪意のある commit を push し self approve / merge できてしまうからです。
reviewer を二人以上にするといった手もありますが、人間の負担が大きくなり開発効率が悪くなると思いますし、 Renovate の Pull Request を自動マージするのにも 2 approve 以上必要になります。

Renovate の Pull Request に変更を加えたくなるケースもあるとは思いますが、その場合新しく Pull Request を人の手で作る必要があります。面倒かもしれませんが、利便性とセキュリティのトレードオフでしょう。

4. Renovate の branch に push する専用の GitHub App を用意する

一部のコードが自動生成されていて、特定の dependency が update された場合にそのコードを自動更新したい場合もあるでしょう。
その場合専用の GitHub App を用意し、その GitHub App が Renovate の branch に push するのを許可する必要があります。
自動 approve の token 同様、その GitHub App の扱いには注意が必要です。

5. (必要であれば) Renovate の branch を update するための workflow を用意する

人間が Renovate の branch に push できない場合、 update branch も当然できなくなります。
Renovate は自動 rebase にも対応していますが、それだと rebase されるまでにタイムラグがある、自動コミットが rebase によって取り消され無駄に CI が走るといった問題もあるため、手動で update branch したいケースはあるでしょう。
その場合 pull request に特定のコメントをすると update branch をする workflow があると便利でしょう。

example workflow

image

6. (必要であれば) GitHub Actions の Self hosted Runner でコストを抑える

status-checkpath-filter といった job が必ず実行されるのがコスト的に気になる人もいるでしょう。
その場合既に Self host Runner が導入されている組織であれば、 Self hosted runner を使うことでコストを抑えられるかもしれません。

public repository のために

1. Fork からの Pull Request でも Workflow が動作するようにする

Renovate とは無関係ですが public repository の場合 Fork からの Pull Request でも Workflow が動作するようにする必要があります。
例えばコードの自動更新や pull request へのコメントの投稿など、 write 権限が必要な処理は実行されないようにする必要があります。

自分の場合以下のような if 文で fork からの Pull Request を分岐しています。

if: |
  github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork

この if 文ではメンテナが trigger した event も skip されますが、これは意図的なものです。
fork からの pull request である以上、危険な変更が含まれている可能性はあるからです。
危険な変更を review で見落とす可能性も当然あります。なので自分は event を起こしたユーザーではなく単純に fork か否かで判別しています。

参考

以下の記事も参考になりますが、一部古い内容も含んでいます。

https://blog.studysapuri.jp/entry/2022/02/18/080000

https://blog.studysapuri.jp/entry/2020/12/10/080000

https://dev.to/suzukishunsuke/tips-about-renovate-38bd

https://devs.quipper.com/2022/03/29/automate-handling-a-number-of-pull-requests-by-renovate-in-terraform-monorepo

Discussion