スタディサプリENGLISH の web フロントエンドプロジェクトで Renovate を半年運用して得た Tips 8選 +α

前置き

スタディサプリENGLISH の web フロントエンドアプリは、実に多くの node モジュールライブラリ(以下、ライブラリ)に支えられています。

当初は開発メンバーが有志でそれらのライブラリを手動アップデートする運用で回していましたが、2021年3月頃より Renovate を本格導入することで依存ライブラリのアップロードを自動化する運びとなりました。

Renovate とは

プロジェクトで依存しているライブラリ等のアップデートを自動化してくれるツール(≒ サービス)です。依存ライブラリのバージョニングを監視し、アップデート版が公開されるとそれに追従するためのプルリクエスト(以下、プルリク)を自動で作成してくれるという優れものです。かつては有償のサービスでしたが1)セルフホスティングすれば無料で使うこともできました。現在は完全に無料化されています。使わない手はありません。

今回は Renovate を半年間ほど運用してきた過程で得た Tips をいくつかご紹介します。

実際に運用している Renovate の設定はこちら(※ 公開向けに一部編集しています)。
{
  extends: ['config:base'],
  timezone: 'Asia/Tokyo',
  labels: ['Dependencies', 'Tech issue'],
  schedule: ['every weekend'],
  // Renovate が同時に作成する branch と PR の数を制限する。
  prConcurrentLimit: 18,
  commitMessagePrefix: 'ci: ',
  reviewers: ['team:e-web-devs'],
  separateMinorPatch: true,
  major: {
    stabilityDays: 7,
  },
  minor: {
    stabilityDays: 3,
  },
  patch: {
    stabilityDays: 2,
    automerge: true,
  },
  vulnerabilityAlerts: {
    labels: ['Security', 'Tech issue'],
    reviewers: ['team:e-web-devs'],
  },
  packageRules: [
    {
      groupName: 'Node.js',
      matchPackageNames: ['node', 'circleci/node', '@types/node'],
      // この正規表現は、下記を表す。
      // - Major: 偶数のみ
      // - Minor: 全て対象
      // - Patch: 0 のみ対象
      allowedVersions: '/^[0-9]+[24680]\\.[0-9]+\\.0$/',
    },
    {
      // CSS Modules のためのパッケージ群は、CSS-in-JS 化が進行中なので積極的なアップデートをやめておく。
      // ただし、パッチはバグ・セキュリティ修正の可能性を認知できるほか、負担も少なそうなので許容する。
      matchPackageNames: [
        'sass',
        'sass-loader',
        'css-loader',
        'mini-css-extract-plugin',
      ],
      major: {
        enabled: false,
      },
      minor: {
        enabled: false,
      },
    },
    {
      // MobX は v4,5 <-> v6 とで互換性が無く、アプリコードの大幅な改修が必要となるため一旦 v5 で止める。
      matchPackageNames: ['mobx'],
      allowedVersions: '<6',
    },
    {
      matchPackageNames: ['mobx-react'],
      allowedVersions: '<7',
    },
    {
      groupName: 'Babel families',
      matchPackagePatterns: ['^babel', '^@babel'],
    },
    {
      groupName: 'Reg Suit families',
      matchPackagePatterns: ['^reg'],
    },
    {
      groupName: 'React families',
      matchPackagePatterns: ['^react', '^@types/react'],
    },
    {
      groupName: 'ESLint families',
      matchPackagePatterns: ['eslint'],
    },
    {
      groupName: 'Prettier families',
      matchPackagePatterns: ['^prettier'],
    },
    {
      groupName: 'Stylelint families',
      matchPackagePatterns: ['stylelint'],
    },
    {
      groupName: 'Storybook families',
      matchPackagePatterns: ['storybook'],
    },
    {
      groupName: 'webpack families',
      matchPackagePatterns: ['terser', 'webpack', '-loader$'],
      // react-content-loader は webpack と無関係なので除外。
      excludePackageNames: ['react-content-loader'],
    },
    // CLI で使うパッケージ。
    {
      groupName: 'CLI families',
      matchPackagePatterns: ['@sentry/cli', 'aws-sdk'],
      // 問題があれば日次のデプロイでわかるはずなので、短めの設定。
      // aws-sdk の更新頻度が高いのも理由。
      stabilityDays: 1,
    },
  ]
}

1. レビュワーは自動でアサインする

普段のコードレビューは GitHub の機能でレビュワーを自動的にアサインしていますが、Renovate が作成するプルリクにもこの運用を適用しています。

我々 web フロントエンド開発メンバーは quipper という GitHub organization 配下の e-web-devs というチームで括られており、これを下記のように設定することで Renovate からのプルリクにも適用できます。

{
  reviewers: ['team:e-web-devs'], // チーム名を指定する際は `team:` という接頭辞を付けること。
}

これで Renovate がプルリクをオープンした時に e-web-devs を Reviewers に指定するようになります。あとは普段の開発プルリクと同じく GitHub が Code review assignment の設定に応じてチームメンバーをレビュワーに自動でアサインしてくれます。こうすることでライブラリのアップデート作業が特定のメンバーに偏ることなく均等に割り当てられるため、チーム一丸となってこの運用に取り組む体制を実現できるというわけです。

GitHub の自動アサイン機能を使わない方法

下記のように定義することで Renovate 自体の機能だけで自動アサインすることも可能です。

{
  reviewers: ["john", "paul", "george", "ringo"],
  reviewersSimpleSize: 1, // レビュワーに自動アサインする人数
}

GitHub の自動アサイン機能に依存しない分こちらの方がシンプルとも言えますが、開発メンバーに変更が入るたびにこの reviewers プロパティを編集することになるため、運用を考慮すると GitHub の自動アサイン機能を利用する方が良いかもしれません。

2. CI/CD のオールグリーン ✅ は必須とする

ライブラリのアップデートが原因でビルドが失敗してしまっては一大事です。そのため弊プロジェクトでは全てのプルリクに対し下記の内容で CI/CD を実施しています。

  1. リポジトリ配下にある全アプリケーションのビルド
    • ※ モノレポ構成のため、一つのリポジトリ配下に複数のアプリケーションがぶら下がっている
  2. Lint
    • ESLint
    • Prettier
    • Stylelint
  3. Unit Test
  4. Visual Regression Test
  5. E2E Test

これらが全て ✅ となることをマージの条件としています。 1 は言わずもがなですが、それ以外の項目も非常に大切です。

弊プロダクトは Emotion という CSS-in-JS を使っているのですが、バージョンアップによってテンプレートリテラルの解釈が変わった影響で期待通りにスタイルが適用されない事案が発生しました。これに気付けたのは Visual Regression Test 基盤を構築していたからです。

また、E2E Test は実際にビルドしたアプリケーションを動かして検証するので、手動による手間が省けるのはもちろん「実際に動かして問題がないかどうか」が分かるという安心感に繋がるメリットがあります。テスト基盤の保守・運用は非常に高コストですが、今日まで Renovate を運用してこれたのはこれらに支えられてきたからであるのは間違いありません。

どうしても無理そうなら潔くアップデートを諦める

時にはライブラリの破壊的変更が激しすぎるためにどうしてもアップデートできない、やるにしてもアプリケーションコードの大幅な書き直しが必要といった場面にも遭遇します。そういう時は潔くアップデートを諦める判断をします。下記は状態管理ライブラリである MobX のバージョンを v5.x で止めている例です。

{
  packageRules: [
    {
      // MobX は v4,5 <-> v6 とで互換性が無く、アプリコードの大幅な改修が必要となるため一旦 v5 で止める。
      matchPackageNames: ['mobx'],
      allowedVersions: '<6',
    },
  ],
}

この他、近い将来破棄することが確定しているライブラリは下記のようにしてアップデートを止めています。

{
  packageRules: [
    {
      // CSS Modules のためのパッケージ群は、CSS-in-JS 化が進行中なので積極的なアップデートをやめておく。
      // ただし、パッチはバグ・セキュリティ修正の可能性を認知できるほか、負担も少なそうなので許容する。
      matchPackageNames: [
        'sass',
        'sass-loader',
        'css-loader',
        'mini-css-extract-plugin',
      ],
      major: {
        enabled: false,
      },
      minor: {
        enabled: false,
      },
    },
  ],
}

3. Patch バージョンのアップデートは CI/CD が通ったら自動でマージ

多くのライブラリはいわゆる semantics versioning に準拠しています。すなわち majer , minor, patch ですが、このうち patch は原則 後方互換性を伴うバグ修正 に相当します。そのため CI/CD はほぼ確実に ✅ となり、開発メンバーが積極的にお世話する必要性も低いことから、 CI/CD が通ったら Renovate 自信が自動でマージする仕組みにしています。よってレビュワーもアサインされません。

{
  patch: {
    automerge: true,
  },
}

automerge プロパティを有効にすると、そのバージョンのアップデートプルリクにはレビュワーがアサインされなくなります。

ちなみに GitHub の Branch protection rules の設定で Require pull request reviews before merging を有効にしているとマージするのにレビュワーからの Approve が必要となりますが、予め renovate-approve という GitHub App を導入しておくとこれが Approve 作業を代替してくれるようになります。

4. 関連するライブラリはグルーピングする

通常、Renovate はライブラリ一つ一つを個別にアップデートする仕様ですが、関連性(≒ 凝集性)の高いライブラリはグルーピングすることで可能な限り一括でアップデートするようにします。

ビルドツールを例に挙げると、現在スタディサプリENGLISH の web フロントエンドは webpack を使ってビルドしています。一言に webpack といっても webpack , webpack-cli , webpack-dev-server と依存するライブラリは複数あります。更に TypeScript をビルドするための ts-loader などもあり、これらはすべて依存関係にあります。よってこれらのアップデートは可能な限り同時に行うのが適切と判断しました。

{
  packageRules: [
    {
      groupName: 'webpack families',
      matchPackagePatterns: ['terser', 'webpack', '-loader$'],
    },
  ],
}

matchPackagePatterns に記述した正規表現に該当したライブラリがこのグルーピングルールに含まれ、それらをまとめたプルリクが作られるようになります。

webpack と webpack-cli をまとめてアップデートするプルリク。

5. Node.js のアップデートは LTS に限定する

ビルド、 Lint、デプロイといった様々なスクリプトを実行するランタイムとして Node.js は必要不可欠です。 package.jsonengines というプロパティを記述することで Renovate の管理対象に含めることができます(.node-version ファイルがあれば、これも併せて更新されます)。

プロジェクトの屋台骨ですからこれのアップデート追従も非常に重要なのですが、Node.js はアップデートのペースが早い傾向にあります。また、アップデートする度に開発メンバーは各自のマシンにある Node.js の最新バージョンのインストールし直しを強いられます。これが地味に面倒。

弊プロジェクトにおける Node.js はタスクランナーのランタイム用途に限定されるため、TypeScript や React と違って常に最新の機能を追い求める必然性は薄いのです。そのためタスクランナーの安定動作さえ保証できれば充分ということで、 LTS ( Long-term Support ) バージョンのアップデートのみに限定することにしました。

{
  packageRules: [
    {
      groupName: 'Node.js',
      matchPackageNames: ['node', 'circleci/node', '@types/node'],
      // この正規表現は、下記を表す。
      // - Major: 偶数のみ
      // - Minor: 全て対象
      // - Patch: 0 のみ対象
      allowedVersions: '/^[0-9]+[24680]\\.[0-9]+\\.0$/',
    },
  ],
}

少々愚直ですが正規表現で LTS のバージョニングルールを表現しました。本来であれば patch アップデートは LTS に含まれますが、少なくとも弊プロジェクトにおいてはそこまで必要ではないと判断し除外しています。これで package.json.node-versionはもちろん、 GitHub Actions や Circle.CI 上で動かす仮想マシン内 Node.js のバージョンもアップデート対象となります。

6. プルリクのオープンは週末に限定する

平日の日中はアプリケーションの機能開発、バグ修正、リファクタリングといった作業に追われているため、ライブラリアップデートのプルリクにまで気を回す余裕には限りがあります。そんな中でドカドカとプルリクを作られるのもなかなかどうしてしんどいものがあります。

ということで、弊チームではRenovate がプルリクをオープンする時間帯を毎週土曜日 0:00 から日曜日 23:59 の範囲に限定しています。

{
  schedule: ['every weekend'],
}

弊チームが稼働していない間に一気にプルリクを作成してもらい、翌営業日に積み上がったものをメンバー各自で捌く運用にすることでメリハリをつけています

7. オープンできるプルリクの数に上限を設ける

オープンするタイミングを週末に制限してるとはいえ、数があまりに多いとそれはそれで辟易してしまいます。そこで Renovate によるプルリクのうち一度に作成できる数(= オープン状態にできる数)の上限を18に制限しています。2021年9月現在の弊チームは6人の web フロントエンドエンジニアが在籍しているため、1人あたりが1週間で受け持つ数が最大3つになるという計算です。

{
  // Renovate が同時に作成する branch とプルリクの数を制限する。
  prConcurrentLimit: 18,
}

1人あたり3つという数に明確な根拠はなく「それくらいなら無理なく対応できそうな気がする」といった緩いものです。事実、月曜の朝にはてんこ盛りだったプルリクも金曜の業務が終わる頃には大抵消化されてることが殆どです。

週明けのプルリク一覧。さぁ頑張って見ていくぞ。

8. プルリクオープン後すぐにマージせず、2 ~ 7日ほど寝かせる

プルリクオープン後に CI/CD が通ってもすぐにはマージしません。メジャーアップデートなら 7日間、マイナーなら3日間、パッチなら2日間そこから寝かせて様子を見ます。

{
  major: {
    stabilityDays: 7,
  },
  minor: {
    stabilityDays: 3,
  },
  patch: {
    stabilityDays: 2,
    automerge: true,
  },
},

著名で比較的規模の大きなライブラリは、最新バージョンがリリースされるとすぐにバグが発見されて修正アップデートが入ることが頻繁に発生します2)とりわけ webpack によく見られる印象ですね。。そういったアップデートが入ると Renovate はオープンしているプルリクに force push してアップデート内容を更新します。このイベントが落ち着くまで様子見するというわけです。

ESLint や Prettier などは IDE プラグイン対応後にマージしないと開発体験が損なわれる

例えば ESLintPrettier といったコードフォーマッタは、IDE にそれ用のプラグインをインストールして使うことが多いかと思います。それらプラグインも原則としてプロジェクト下にインストールされているライブラリを参照して動作しますから、最新バージョンの仕様に対応していることが前提となり、もし対応が追いついてないと期待通りに動作しなくなる恐れがあります。つまり関連するプラグインも常にアップデートしてもらう必要があるわけです。

大抵は殆ど間隔が開くことなく対応してくれますが、それでも数日を要することが多いです。過去の失敗談として、ESLint のマイナーアップデートプルリクがオープンした直後にマージしたら、VS Code のエクステンションが正しいはずのコードに大量のエラー判定を出してきたことがありました。翌日にはエクステンションもアップデートされて治まったものの、その日はまともにコーディングできなかったのをよく覚えています。

その他: 導入して得た思いがけない副作用(恩恵)

使っていないライブラリの存在に気付けた

あまりないケースかもしれませんが、Renovate を導入していたことで既に使われていないライブラリの存在に気付いてそのままアンインストールするという事例がいくつかありました。

現在の弊プロジェクト(リポジトリ)は最初の作成から実に3年以上も経っており、そこから現在のアーキテクチャに落ち着くまで何度も試行錯誤を繰り返してきました。その過程で、導入したがいつの間にか使われなくなり忘れ去られるライブラリというのが出てきてしまっていたというわけです。

Renovate からすればプロジェクト内で使われていまいがバージョンが古ければアップデートを促すしかありません。オープンされたプルリクを見て「こんなライブラリあったっけ…?」となれば、何かしらの理由で既に使われていない可能性のあるものと見なすことができ、棚卸しするチャンスです。

締め

Renovate に関する知見はググってもそれほど多くヒットせず、多くが導入手順(= Getting Started)で終わっているものばかりな印象でした。そのため我々なりに試行錯誤を重ねてどうにか軌道に乗せるにまで至ったわけですが、この記事が少しでも何かの参考になれば幸いです。

脚注

脚注
1 セルフホスティングすれば無料で使うこともできました。
2 とりわけ webpack によく見られる印象ですね。